# Telemetry API

> Geçmiş veri deposundaki veri noktası değerlerini HTTP üzerinden tek bir SQL SELECT ile sorgulayın ve sonucu JSON satırları olarak okuyun.

Kaynak: https://proxus.io/tr/docs/develop/telemetry-api

Bu kılavuzu tamamladığınızda betiğiniz, bir cihazın saklanan veri noktası değerleri üzerinde SQL sorgusu çalıştırır.
Sonuçta her satır ayrı bir JSON nesnesi olarak gelir.

## Başlamadan önce

Şunlar gereklidir:

- Merkez sunucudan alınmış bir erişim anahtarı. Nasıl alınacağı [REST API](https://proxus.io/tr/docs/develop/rest-api#adım-1-erişim-anahtarı-alın)
  sayfasında anlatılır.
- Merkez sunucunun adresi, örneğin `http://localhost:8080`.
- Telemetri verisini ClickHouse'ta saklayan bir merkez sunucu. Ürünle gelen Docker kurulumu bu veritabanını
  kullanır (yapılandırmanın `[Database]` bölümünde `TelemetryDB = "clickhouse"`).

> **Uyarı:**
Telemetry API, gönderilen SQL'i doğrudan çalıştırır. Bu nedenle kullanıcının UNS izinlerini
uygulayamaz. API'yi yalnızca yönetici rolüne sahip kullanıcılar çağırabilir, diğer kullanıcılar `403` alır. Bu
erişimi yalnızca güvendiğiniz sistemlere verin. Diğer kullanıcılar değerleri,
[UNS izinlerini](https://proxus.io/tr/docs/administer/uns-permissions) uygulayan ekranlardan okur.

## İstek

```text
POST /api/Telemetry/execute?maxRows=10000
Authorization: Bearer <token>
Content-Type: application/json

{ "query": "SELECT ..." }
```

| Bölüm | Anlamı |
| --- | --- |
| `query` | ClickHouse SQL söz dizimiyle yazılmış tek bir ifade. |
| `maxRows` | Döndürülecek en çok satır sayısı. 1 ile 50.000 arasında olabilir, varsayılan değer 10.000'dir. Sorgudaki `LIMIT` bu değeri aşarsa bu değere indirilir. `LIMIT` içermeyen sorgulara otomatik olarak eklenir. |

Yanıtın içerik türü `application/x-ndjson` olur: her sonuç satırı ayrı bir satırda, tek bir JSON nesnesi olarak
gelir. Sorgu bir `FORMAT` ifadesiyle bitmiyorsa sonuna otomatik olarak `FORMAT JSONEachRow` eklenir.

## Saklanan değerler tablosu

Proxus'un sakladığı her değer, `DeviceRawData` tablosunda bir satırdır:

| Sütun | Tür | Anlamı |
| --- | --- | --- |
| `Time` | `DateTime64(3)` | Değerin okunduğu an. UTC olarak, milisaniye hassasiyetinde tutulur. |
| `DeviceId` | `UInt32` | Cihazın ID'si. [REST API](https://proxus.io/tr/docs/develop/rest-api)'deki `Oid` değeriyle aynıdır. |
| `DeviceName` | `String` | Okuma anında cihazın taşıdığı ad. |
| `Key` | `String` | Veri noktasının adı. |
| `DataType` | `Enum8` | `Bool`, `Short`, `UShort`, `Int`, `UInt`, `Float`, `Long`, `ULong`, `Double`, `String`, `Byte`, `DateTime`. |
| `Unit` | `String` | Veri noktasının birimi. Birim tanımlı değilse boştur. |
| `NumericValue` | `Float64` | Sayısal ve Boolean veri noktalarının değeri. |
| `StringValue` | `String` | Metin türündeki veri noktalarının değeri. |

Satırlar cihaz, ad, veri noktası ve zaman sırasıyla dizilir. Bu yüzden en hızlı sorgular, `DeviceId` veya
`DeviceName`, `Key` ve bir zaman aralığıyla süzülen sorgulardır.

## Adım 1: Bir sorgu çalıştırın

```bash
TOKEN=$(curl -s -X POST http://localhost:8080/api/Authentication/Authenticate \
  -H "Content-Type: application/json" -d '{"userName":"Admin","password":""}')

curl -s -X POST "http://localhost:8080/api/Telemetry/execute?maxRows=100" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d "{\"query\": \"SELECT DeviceName, Key, round(avg(NumericValue), 2) AS avg_value, count() AS samples FROM DeviceRawData WHERE Time >= now() - INTERVAL 1 HOUR AND DeviceName = 'LYS_PACKER_04' GROUP BY DeviceName, Key ORDER BY Key\"}"
```

Yanıt şöyle görünür:

```json
{"DeviceName":"LYS_PACKER_04","Key":"BatchCount","avg_value":1522.98,"samples":"2262"}
{"DeviceName":"LYS_PACKER_04","Key":"MotorCurrent","avg_value":28.91,"samples":"2262"}
{"DeviceName":"LYS_PACKER_04","Key":"Speed","avg_value":81.68,"samples":"2262"}
{"DeviceName":"LYS_PACKER_04","Key":"Temperature","avg_value":24.64,"samples":"2262"}
{"DeviceName":"LYS_PACKER_04","Key":"Vibration","avg_value":1.85,"samples":"2262"}
```

> **Not:**
`count()` sonucu gibi 64 bitlik tamsayılar metin olarak gelir (`"2262"`). Böylece JSON'a dönüştürülürken basamak
kaybı olmaz. Bu değerleri kodunuzda sayıya çevirin ya da SQL içinde dönüştürün (`toUInt32(count())`).

## Adım 2: Koddan kullanın

```python
import json
import requests

HUB = "http://localhost:8080"

token = requests.post(
    f"{HUB}/api/Authentication/Authenticate",
    json={"userName": "Admin", "password": ""},
    timeout=30,
).text.strip()

sql = """
SELECT Key, round(avg(NumericValue), 2) AS avg_value, max(NumericValue) AS max_value
FROM DeviceRawData
WHERE DeviceName = 'LYS_PACKER_04'
  AND Time >= now() - INTERVAL 1 HOUR
GROUP BY Key
ORDER BY Key
"""

response = requests.post(
    f"{HUB}/api/Telemetry/execute",
    params={"maxRows": 1000},
    json={"query": sql},
    headers={"Authorization": f"Bearer {token}"},
    timeout=60,
)
response.raise_for_status()
for line in response.text.splitlines():
    row = json.loads(line)
    print(row["Key"], row["avg_value"], row["max_value"])
```
```csharp
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;

var hub = new HttpClient { BaseAddress = new Uri("http://localhost:8080/") };

var login = await hub.PostAsJsonAsync("api/Authentication/Authenticate",
    new { userName = "Admin", password = "" });
login.EnsureSuccessStatusCode();
hub.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", await login.Content.ReadAsStringAsync());

// Bir cihazın her veri noktasının son değeri
var query = new {
    query = """
            SELECT Key, argMax(NumericValue, Time) AS last_value, max(Time) AS last_time
            FROM DeviceRawData
            WHERE DeviceName = 'LYS_PACKER_04' AND Time >= now() - INTERVAL 1 HOUR
            GROUP BY Key ORDER BY Key
            """
};
var response = await hub.PostAsJsonAsync("api/Telemetry/execute?maxRows=100", query);
response.EnsureSuccessStatusCode();
foreach (var line in (await response.Content.ReadAsStringAsync()).Split('\n', StringSplitOptions.RemoveEmptyEntries)) {
    using var row = JsonDocument.Parse(line);
    Console.WriteLine($"{row.RootElement.GetProperty("Key")} = {row.RootElement.GetProperty("last_value")}");
}
```

## Uzun bir zaman aralığını okuyun

Tek bir çağrı en çok 50.000 satır döndürür. Daha fazla veri okumak için sonucu zamana göre sayfalara bölün. Sonucu `Time` ve
`DeviceId` ile sıralayın, son satırı not edin ve sonraki sorguyu o satırın ardından başlatın.

```sql
SELECT Time, DeviceId, Key, NumericValue
FROM DeviceRawData
WHERE DeviceName = 'LYS_PACKER_04'
  AND Time >= toDateTime64('2026-10-01 00:00:00', 3)
  AND Time <  toDateTime64('2026-10-02 00:00:00', 3)
  AND (Time > toDateTime64('2026-10-01 06:12:40.250', 3)
       OR (Time = toDateTime64('2026-10-01 06:12:40.250', 3) AND DeviceId > 69))
ORDER BY Time ASC, DeviceId ASC
LIMIT 50000
```

Bir sayfa, istenenden az satır döndürdüğünde okuma tamamlanmıştır.

## Reddedilen sorgular

Sorgu, veritabanına gönderilmeden önce denetlenir. Aşağıdaki yanıtlar `400` durum koduyla döner:

| Mesaj | Nedeni |
| --- | --- |
| `Query cannot be empty` | İstek gövdesinde `query` alanı yok. |
| `Invalid query: Only SELECT/CTE (WITH), comments, or 'SET TIMEZONE' are allowed at the beginning.` | İfade izin verilmeyen bir komutla başlıyor, örneğin `SHOW` veya `DESCRIBE`. |
| `Invalid query: Multiple statements are not allowed.` | Sorguda `;` ile ayrılmış birden çok ifade var. |
| `Invalid query: After CTEs, only a SELECT statement is allowed.` | `WITH` bloğundan sonra `SELECT` dışında bir ifade geliyor. |
| `Invalid query: Security violation: The SQL keyword 'SYSTEM' is not permitted.` | Sorgu, veriyi veya sunucuyu değiştiren bir sözcük içeriyor: `UPDATE`, `DELETE`, `INSERT`, `MERGE`, `UPSERT`, `DROP`, `CREATE`, `ALTER`, `TRUNCATE`, `RENAME`, `ATTACH`, `DETACH`, `OPTIMIZE`, `SYSTEM`, `KILL`, `GRANT`, `REVOKE`, `COPY`, `CALL`, `EXEC`, `EXECUTE`. Denetim tablo ve sütun adlarını da kapsar, bu yüzden `system.tables` de reddedilir. Tırnak içindeki sözcükler denetlenmez. |

Diğer yanıtlar:

| Durum | Anlamı |
| --- | --- |
| `401` | Erişim anahtarı gönderilmemiş ya da süresi dolmuş. |
| `403` | Kullanıcının yönetici rolü yok. |
| Veritabanının döndürdüğü durum kodu ve `Query failed: ...` | Veritabanı sorguyu reddetti, örneğin bir sütun veya tablo adı yanlış yazılmış. Metin, veritabanının kendi hata mesajıdır. |
| `500` ve `ClickHouse connection failed: ...` | Merkez sunucu telemetri veritabanına ulaşamıyor. Çözüm için [Veri sorunlarını giderme](https://proxus.io/tr/docs/troubleshoot/data) sayfasına bakın. |

## Sonraki adımlar

- [MQTT ve UNS konuları](https://proxus.io/tr/docs/develop/mqtt): değerleri sorgulamak yerine canlı olarak alın.
- [Geçmiş veri aktarımı](https://proxus.io/tr/docs/develop/historical-import): aynı tabloya eski değerleri ekleyin.
- [Raporlar](https://proxus.io/tr/docs/analyze/reports): aynı veriden hazırlanmış hazır raporları kullanın.
