Перейти до змісту

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.

curl -s https://jaam.net.ua/alerts_statuses_v3.json

Застарілі ендпоінти

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

Життєвий цикл з'єднання

sequenceDiagram participant D as Пристрій participant S as Сервер D->>S: WebSocket handshake D->>S: chip_id:<ID> D->>S: firmware:<версія> D-->>S: user_info:<JSON> (опційно) S->>D: початкові пакети loop постійно S->>D: ping D->>S: pong S->>D: оновлення даних end

Тримайте з'єднання відкритим

Сенс 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.

Правила розбору:

  1. Перевіряйте довжину: якщо тіло не ділиться на розмір запису, пропустіть кадр (не падайте і не розривайте з'єднання).
  2. Невідомий тип - ігноруйте.
  3. Один регіон може з'явитися в кількох пакетах: пізніший пакет перезаписує попередній стан цього регіону.

Готовий розбір (перевірений на пакетах, які будує сервер):

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 зазвичай працює багато пристроїв, тож блокування б'є й по сусідніх.

Вимоги:

  1. Між спробами - експоненційна затримка: 5 с → 10 с → 20 с → 40 с → … до 5 хвилин.
  2. До затримки додавайте випадкове відхилення (jitter) ±20%, щоб після збою сервера всі пристрої не повернулись одночасно.
  3. Затримку скидайте лише після того, як з'єднання пропрацювало не менше хвилини. З'єднання, що закрилось за секунди, - це не успіх.
  4. Не відкривайте кілька паралельних з'єднань з одного пристрою.
  5. Після відмови або закриття з кодом 1008 не повторюйте спробу одразу: збільште затримку до максимуму.
  6. Розрив у момент втрати 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-клієнт, передайте йому цей файл разом із вашим завданням:

Завантажити інструкцію для AI

Файл самодостатній: у ньому всі вимоги з цієї сторінки (перший пакет, ping/pong, перепідключення) у формі, зручній для AI.

Tip

Після того як AI згенерує код, перевірте його за чекліст вище. Саме на ньому найчастіше пропускають затримку перепідключення.