API сервера даних¶
Ця сторінка описує, як правильно користуватися публічним API сервера даних JAAM: REST (JSON і картинки) та WebSocket (дані в реальному часі, саме так працюють прошивки).
Порушення правил призводить до блокування
Зловживання API карається. Спершу обмежується пристрій (за його Chip ID), а якщо порушення не припиняються - IP-адреса, з якої йдуть запити.
Блокування мовчазне: сервер не повідомляє причину - з'єднання просто лишається без даних або закривається. Тому дотримуйтесь вимог нижче з самого початку, а не після того, як "перестало працювати".
Якщо ви пишете власний клієнт - обов'язково реалізуйте перший пакет, відповідь на ping та перепідключення з затримкою.
REST API¶
Базова адреса: https://jaam.net.ua. Усі відповіді - JSON у UTF-8 (крім .png і .plain). Авторизація не потрібна.
Ендпоінти¶
| Ендпоінт | Що повертає |
|---|---|
/alerts_statuses_v1.json |
Тривоги по областях: enabled, district, enabled_at |
/alerts_statuses_v2.json |
Тривоги по областях: alertnow, district, changes |
/alerts_statuses_v3.json |
Найкомпактніший: {"states": {"<область>": true/false}} |
/weather_statuses_v1.json |
Погода по областях: температура, опис, тиск, вологість, вітер |
/weather_statuses_v2.json |
Розширені погодні дані, ключ - id регіону |
/tcp_statuses_v1.json |
Тривоги й погода одним рядком у JSON (tcp_stored_data) |
/tcp_statuses_v2.plain |
Те саме, звичайним текстом |
/api_status.json |
Скільки секунд тому оновилися джерела (тривоги, погода, енергосистема, радіація) |
/alerts_map.png, /weather_map.png |
Готові картинки мапи |
Повний опис полів - у Swagger: https://jaam.net.ua/api.
Застарілі ендпоінти
explosives_statuses_*.json, missiles_statuses_*.json, drones_statuses_*.json, kabs_statuses_*.json повертають 410 Gone. Не використовуйте їх і не опитуйте повторно.
Вимоги до REST-клієнта¶
- Частота: не частіше ніж раз на 30 секунд для одного ендпоінта. Дані на сервері оновлюються не миттєво, тож частіші запити нічого не дають, а лише створюють навантаження.
- Для оновлень "у реальному часі" використовуйте WebSocket, а не часте опитування REST.
- Не опитуйте кілька версій одного й того самого ендпоінта - оберіть одну.
- Обробляйте помилки (
5xx, таймаут) повтором із наростаючою затримкою, а не негайним повтором у циклі. - Вказуйте осмислений
User-Agent, щоб вас можна було відрізнити від ботів.
Вказуйте джерело даних¶
Якщо ви використовуєте наше API у власному застосунку, на сайті, в боті чи прошивці - вкажіть це явно: наприклад, "Дані: JAAM (jaam.net.ua)" з посиланням на https://jaam.net.ua. Розмістіть це там, де користувач бачить дані (підвал сайту, екран "Про застосунок", опис проєкту).
WebSocket API¶
Адреси¶
| Параметр | Значення |
|---|---|
| Хост | ws.jaam.net.ua (порт 80, ws://; також wss:// на 443) |
| Шлях | /data_fusion_v1 |
Приклад: ws://ws.jaam.net.ua/data_fusion_v1
Життєвий цикл з'єднання¶
Тримайте з'єднання відкритим¶
Сенс WebSocket - одне довге з'єднання, по якому сервер сам надсилає оновлення в момент зміни даних. Тому:
- Підключились один раз і тримайте з'єднання відкритим. Не розривайте його після отримання початкових даних.
- Не використовуйте WebSocket як аналог опитування (polling): "підключився → отримав дані → відключився → через хвилину знову". Це створює зайве навантаження і вважається зловживанням - за це блокують.
- Якщо вам потрібні дані раз на деякий час, а постійне з'єднання не потрібне - використовуйте REST з дозволеною частотою.
- З'єднання розривається лише коли воно справді впало або застосунок завершується.
Перший пакет від пристрою¶
Одразу після встановлення з'єднання клієнт сам надсилає два текстові повідомлення, у цьому порядку:
| # | Повідомлення | Приклад | Обов'язкове |
|---|---|---|---|
| 1 | chip_id:<ID> |
chip_id:A1B2C3D4E5F6 |
так |
| 2 | firmware:<версія> |
firmware:2.4.1 |
так |
| 3 | user_info:<JSON> |
user_info:{"chip":"ESP32-S3"} |
ні |
Вимоги:
chip_id- унікальний і стабільний ідентифікатор пристрою (для ESP32 - MAC-адреса без роздільників, великими літерами). Не генеруйте випадковий при кожному запуску і не використовуйте чужий.- Обидва повідомлення (
chip_idіfirmware) потрібно надіслати протягом 10 секунд після з'єднання. Інакше сервер закриє з'єднання, не надіславши жодних даних. - Формат -
<ключ>:<значення>, роздільник - перша двокрапка. Значення не повинно містити переносів рядків. user_info- JSON; сервер використовує лише полеchip(модель чіпа:ESP32,ESP32-S3,ESP32-C3). Невалідний JSON не розриває з'єднання.
Що надсилає сервер¶
Після першого пакета сервер надсилає початкові дані, а далі - оновлення в момент зміни. Усі повідомлення бінарні (little-endian), перший байт - тип:
| Байт | Дані |
|---|---|
0xA1 |
Тривоги по регіонах |
0xA2 |
Сповіщення (загрози: ракети, дрони тощо) |
0xA3 |
Погода |
0xA4 |
Енергосистема |
0xA5 |
Радіація |
0xA6 / 0xA7 |
Список бета / стабільних версій прошивки |
Початкові дані приходять у порядку: тривоги → погода → енергосистема → радіація → версії прошивки. Невідомі типи ігноруйте, а не розривайте з ними з'єднання: набір може розширюватись.
Note
Сервер не очікує від клієнта жодних команд після першого пакета. Додатковий трафік від клієнта не потрібен.
Формат пакетів і розбір¶
Усі числа - little-endian. Кожне повідомлення: [тип: 1 байт][тіло]. Тіло - послідовність записів фіксованого розміру. region_id - числовий ідентифікатор регіону (u16, той самий, що у джерелі тривог; наприклад 31 - м. Київ). Для радіації є ще спеціальний 8888 - Прип'ять.
| Тип | Дані | Тіло | Повний стан чи зміни |
|---|---|---|---|
0xA1 |
Тривоги | hash_actual u16, hash_previous u16, далі записи region_id u16, flags u16 |
перший пакет - повний стан, далі - лише змінені регіони |
0xA2 |
Сповіщення | записи region_id u16, flags u16 |
подія (одноразове сповіщення, а не стан) |
0xA3 |
Погода | записи region_id u16, temp u8 |
повний стан |
0xA4 |
Енергосистема | записи region_id u16, state u8 |
повний стан |
0xA5 |
Радіація | записи region_id u16, value u16 |
повний стан |
0xA6, 0xA7 |
Версії прошивки (бета / стабільні) | записи major u8, minor u8, patch u8, beta u16 |
повний список |
Тривоги (0xA1). flags - бітова маска:
| Біт | Значення | Біт | Значення |
|---|---|---|---|
| 0 | повітряна тривога | 6 | ракетна загроза |
| 1 | артилерійська небезпека | 11 | рівень тривоги: жовтий |
| 2 | вуличні бої | 12 | рівень тривоги: червоний |
| 3 | хімічна небезпека | ||
| 4 | ядерна небезпека | ||
| 5 | загроза дронів |
- Регіон із
flags = 0означає, що тривога там закінчилась: приберіть його зі свого стану. - Регіон, якого немає в пакеті, не змінився.
- Контроль цілісності. Кожен діф-пакет має
hash_actual(сума байтів його записів mod 65536) таhash_previous(цеhash_actualпопереднього пакета). Якщоhash_previousнового пакета не збігається зhash_actualостаннього отриманого - ви пропустили пакет: ваш стан недостовірний, перепідключіться (за правилами перепідключення), щоб отримати повний стан. Для найпершого пакета після підключення перевірку не робіть - просто запам'ятайте йогоhash_actual.
Сповіщення (0xA2). flags - біти типу події: 5 - дрон, 6 - ракета, 7 - КАБ, 8 - балістика, 9 - вибух, 10 - розвідувальний дрон. Це разова подія для регіону, показуйте її певний час, а не зберігайте як стан.
Погода (0xA3). temp - температура в °C в одному байті: біт 7 - знак (1 = мінус), біти 0-6 - модуль (0-127). 25 → +25, 0x8C (140) → -12. Значення 0 може означати і 0 °C, і відсутність даних.
Енергосистема (0xA4). state - код стану від джерела (Укренерго), сервер передає його без змін. Код, якого ви не знаєте, показуйте як «невідомо», а не як «все добре».
Радіація (0xA5). value - ціле значення радіаційного фону (0-2000), найвище в регіоні.
Версії прошивки (0xA6/0xA7). beta = 0 - стабільна версія major.minor.patch; інакше це бета major.minor.patch-b<beta>. Кількість записів = (довжина - 1) / 5.
Правила розбору:
- Перевіряйте довжину: якщо тіло не ділиться на розмір запису, пропустіть кадр (не падайте і не розривайте з'єднання).
- Невідомий тип - ігноруйте.
- Один регіон може з'явитися в кількох пакетах: пізніший пакет перезаписує попередній стан цього регіону.
Готовий розбір (перевірений на пакетах, які будує сервер):
import struct
ALERT_FLAGS = {
0: "air", 1: "artillery", 2: "urban_fights", 3: "chemical", 4: "nuclear",
5: "drones", 6: "missile", 11: "level_yellow", 12: "level_red",
}
NOTIFY_FLAGS = {5: "drone", 6: "rocket", 7: "kab", 8: "ballistic", 9: "explosion", 10: "recon_drone"}
def _pairs(body: bytes, fmt: str):
size = struct.calcsize(fmt)
if len(body) % size:
raise ValueError("bad length")
return list(struct.iter_unpack(fmt, body))
def decode_temp(mask: int) -> int:
return -(mask & 0x7F) if mask & 0x80 else mask
class State:
def __init__(self):
self.alerts, self.weather, self.grid, self.radiation = {}, {}, {}, {}
self.beta, self.stable = [], []
self.last_hash = None # hash_actual останнього пакета тривог
def feed(self, msg: bytes):
"""Повертає False, якщо треба перепідключитись (пропущено пакет тривог)."""
if not msg:
return True
kind, body = msg[0], msg[1:]
try:
if kind == 0xA1:
h_actual, h_prev = struct.unpack("<HH", body[:4])
records = _pairs(body[4:], "<HH")
initial = self.last_hash is None
if not initial and h_prev != self.last_hash:
return False # пропущено пакет - стан тривог недостовірний
for rid, flags in records:
if flags:
self.alerts[rid] = flags
else:
self.alerts.pop(rid, None)
self.last_hash = h_actual
elif kind == 0xA3:
self.weather.update({rid: decode_temp(m) for rid, m in _pairs(body, "<HB")})
elif kind == 0xA4:
self.grid.update(_pairs(body, "<HB"))
elif kind == 0xA5:
self.radiation.update(_pairs(body, "<HH"))
elif kind in (0xA6, 0xA7):
versions = []
for ma, mi, pa, beta in _pairs(body, "<BBBH"):
versions.append(f"{ma}.{mi}.{pa}" + (f"-b{beta}" if beta else ""))
(self.beta if kind == 0xA6 else self.stable)[:] = versions
elif kind == 0xA2:
return [(rid, [n for b, n in NOTIFY_FLAGS.items() if f >> b & 1]) for rid, f in _pairs(body, "<HH")]
except (ValueError, struct.error):
pass # битий кадр ігноруємо, з'єднання не рвемо
return True
Ping/pong¶
- Сервер сам надсилає ping кожні 20 секунд і чекає на pong до 20 секунд. Без відповіді з'єднання закривається.
- Це стандартні службові кадри WebSocket (control frames). У більшості бібліотек клієнт відповідає на ping автоматично, тож вручну нічого робити не потрібно - переконайтесь, що ваша бібліотека це вміє і що цикл читання повідомлень не заблокований (інакше pong не буде надіслано).
- Не імітуйте pong текстовим повідомленням,
ping/pongне є командами протоколу. - Не надсилайте власні ping частіше, ніж раз на 20 секунд - у цьому немає потреби.
- Якщо ви самі не бачили жодних даних і ping довго - вважайте з'єднання мертвим і перепідключіться за правилами нижче.
Перепідключення¶
Найчастіша причина блокування
Клієнт, який після розриву негайно підключається знову, у циклі, без затримки, буде заблокований - спершу пристрій, потім IP. На одній IP зазвичай працює багато пристроїв, тож блокування б'є й по сусідніх.
Вимоги:
- Між спробами - експоненційна затримка: 5 с → 10 с → 20 с → 40 с → … до 5 хвилин.
- До затримки додавайте випадкове відхилення (jitter) ±20%, щоб після збою сервера всі пристрої не повернулись одночасно.
- Затримку скидайте лише після того, як з'єднання пропрацювало не менше хвилини. З'єднання, що закрилось за секунди, - це не успіх.
- Не відкривайте кілька паралельних з'єднань з одного пристрою.
- Після відмови або закриття з кодом
1008не повторюйте спробу одразу: збільште затримку до максимуму. - Розрив у момент втрати Wi-Fi - нормальна ситуація; чекайте на відновлення мережі, а не підключайтесь у цикл.
Мінімальний клієнт (Python)¶
import asyncio
import random
import time
import websockets
URL = "ws://ws.jaam.net.ua/data_fusion_v1"
CHIP_ID = "A1B2C3D4E5F6"
FIRMWARE = "my-client-1.0"
async def run():
delay = 5
while True:
started = time.monotonic()
try:
# ping надсилає сервер, pong відповідає бібліотека
async with websockets.connect(URL, ping_interval=None) as ws:
await ws.send(f"chip_id:{CHIP_ID}")
await ws.send(f"firmware:{FIRMWARE}")
state = State() # клас State з розділу "Формат пакетів і розбір"
async for message in ws:
if isinstance(message, bytes) and state.feed(message) is False:
break # пропущено пакет тривог - перепідключення за правилами backoff
except (OSError, websockets.ConnectionClosed):
pass
if time.monotonic() - started > 60:
delay = 5 # з'єднання трималось, не штрафуємо себе
await asyncio.sleep(delay * random.uniform(0.8, 1.2))
delay = min(delay * 2, 300)
asyncio.run(run())
Чекліст¶
- З'єднання тримається відкритим, WebSocket не використовується як polling
-
chip_idіfirmwareнадсилаються першими, протягом 10 с -
chip_idстабільний і унікальний - Клієнт відповідає на ping (цикл читання не блокується)
- Перепідключення з експоненційною затримкою та jitter
- Одне з'єднання на пристрій
- Невідомі типи та биті кадри ігноруються
- Для тривог перевіряється ланцюжок
hash_previous→hash_actual - REST опитується не частіше ніж раз на 30 с (або замість нього використовується WebSocket)
- У вашому застосунку/на сайті вказано, що дані надає JAAM
Інструкція для AI¶
Щоб AI-асистент (Claude, ChatGPT тощо) написав вам коректний WebSocket-клієнт, передайте йому цей файл разом із вашим завданням:
Файл самодостатній: у ньому всі вимоги з цієї сторінки (перший пакет, ping/pong, перепідключення) у формі, зручній для AI.
Tip
Після того як AI згенерує код, перевірте його за чекліст вище. Саме на ньому найчастіше пропускають затримку перепідключення.