Files
aircarto-protocols/formats/json-payload.md
Your Name c20ea823e9 docs(miotiq): repurpose payload byte 9 as command (ping test trigger)
Le champ `version` (offset 9 du payload NebuleAir Pro 4G) était hardcodé
`0x01` côté firmware et n'a jamais porté un vrai versioning protocole.
Renommé en `command` et réaffecté à un type de trame :

- 0x00 = données mesure
- 0x01 = ping test (firmware → Miotiq → backend, pas d'archivage)

Versioning protocole reste sur `version_major/minor/patch`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 16:22:27 +02:00

252 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Format JSON canonique — mesures capteurs AirCarto (2026)
JSON envoyé par les capteurs AirCarto (ou par le webhook Miotiq en leur nom) au serveur central.
## Endpoint
```
POST https://api.aircarto.com/receive_data?device_type=<modèle>
Content-Type: application/json
```
Le **type de capteur** est passé en query string. Modèles supportés :
| `device_type` | Description |
|------------------|--------------------------------------------|
| `NebuleAir` | Station fixe NebuleAir |
| `NebuleAir_Pro` | Station fixe NebuleAir Pro (4G) |
| `ModuleAir` | Module AirCarto |
| `ModuleAir_Pro` | Module AirCarto Pro (4G) |
| `MobileAir` | Capteur mobile |
## Règles générales
- **Tous les champs sont optionnels** sauf `device_id`. Un capteur n'envoie que les champs qu'il mesure.
- **Valeur sentinelle `-1`** : donnée non disponible ou capteur non renseigné (ex. pas de GPS fix, pas de pression).
- **Valeur sentinelle `255` (`0xFF`)** sur `error_flags`, `npm_status`, `device_status` uniquement : le firmware du capteur est antérieur à l'introduction du champ. À interpréter comme « non disponible », **pas** comme « toutes les erreurs actives ». Les nouveaux firmwares initialisent ces octets et envoient une valeur ≤ 254.
- **Champs `<nom>_unit`** : optionnels, ajoutés par Miotiq quand la colonne `units` du descripteur est remplie et que l'export JSON est `Y` (voir [`udp-miotiq.md`](udp-miotiq.md)). Format : `"<valeur> <unité>"`. Le backend peut les ignorer, la valeur canonique est toujours le champ sans suffixe.
## Identification
| Champ | Type | Description |
|------------------|--------|--------------------------------------------------------------------------------------------------|
| `device_id` | string | Identifiant unique, **représentation hexadécimale** de 16 caractères (8 octets ASCII). Convertir hex → ASCII pour obtenir le numéro de série imprimé sur le boîtier. Ex. `"4430353234313938"``"D0524198"`. |
| `signal_quality` | int | Qualité du signal réseau (dB, souvent RSSI négatif). |
| `command` | int | Type de trame. `0x00` = données mesure (les autres champs sont valides). `0x01` = ping test (trame émise volontairement par le firmware pour vérifier le lien capteur → Miotiq → backend ; le backend ne doit pas archiver les mesures associées). Voir [Commande / type de trame](#commande--type-de-trame). |
## Commande / type de trame
Le champ `command` (1 octet, offset 9 du payload binaire) discrimine la nature de la trame.
| Valeur | Sens | Action backend attendue |
|---------|--------------|---------------------------------------------------------------------------------------------------------------|
| `0x00` | données | Trame de mesure normale. Décoder et persister les champs métier comme d'habitude. |
| `0x01` | ping test | Trame de diagnostic émise par le firmware pour vérifier le chemin capteur → Miotiq → backend. Logger la réception (timestamp, `device_id`, `signal_quality`) puis **ne pas archiver** les autres champs comme mesures réelles — leur contenu n'est pas garanti significatif. |
Toute autre valeur doit être traitée comme une trame de données (`0x00`) en attendant qu'elle soit officiellement allouée — éviter de rejeter la trame sur la seule base d'un `command` inconnu pour rester forward-compatible.
**Note historique** : ce champ s'appelait `version` avant 2026-04-27 et était hardcodé `0x01` côté firmware. Le versioning du protocole est exclusivement porté par `version_major/minor/patch` (voir [Version firmware](#version-firmware)).
## Polluants (codes ISO LCSQA)
Le mapping complet vit dans [`formats/iso-pollutant-codes.md`](iso-pollutant-codes.md). Liste rappel :
| Champ | Grandeur | Unité |
|-----------|--------------|--------|
| `ISO_68` | PM1 | µg/m³ |
| `ISO_39` | PM2.5 | µg/m³ |
| `ISO_24` | PM10 | µg/m³ |
| `ISO_54` | Température | °C |
| `ISO_55` | Humidité | % |
| `ISO_53` | Pression | hPa |
| `ISO_03` | NO₂ | ppb |
| `ISO_05` | H₂S | ppb |
| `ISO_21` | NH₃ | ppb |
| `ISO_04` | CO | ppb |
| `ISO_08` | O₃ | ppb |
Les codes ISO vont théoriquement de `ISO_01` à `ISO_99`. Seuls les polluants effectivement mesurés par le capteur sont présents dans le JSON.
## Bruit
| Champ | Unité | Description |
|-------------------|-------|-----------------------------------------------------|
| `noise_cur_leq` | dB | Niveau sonore équivalent continu (Leq) courant. |
| `noise_cur_level` | dB | Niveau sonore instantané courant. |
| `max_noise` | dB | Niveau sonore maximal sur la période. |
## Comptage particulaire NPM (Naneos Partector)
| Champ | Unité | Description |
|----------------|-------|--------------------------------------|
| `npm_ch1` | count | Comptage canal 1. |
| `npm_ch2` | count | Comptage canal 2. |
| `npm_ch3` | count | Comptage canal 3. |
| `npm_ch4` | count | Comptage canal 4. |
| `npm_ch5` | count | Comptage canal 5. |
| `npm_temp` | °C | Température interne du module NPM. |
| `npm_humidity` | % | Humidité interne du module NPM. |
| `npm_status` | int | Statut NPM — bitfield, voir ci-dessous. |
## Alimentation
| Champ | Unité | Description |
|-------------------|-------|------------------------------------|
| `battery_voltage` | V | Tension batterie. |
| `battery_current` | A | Courant batterie. |
| `solar_voltage` | V | Tension panneau solaire. |
| `solar_power` | W | Puissance panneau solaire. |
| `charger_status` | int | Code de statut du chargeur MPPT. |
## Vent
| Champ | Unité | Description |
|------------------|---------|-----------------------------------|
| `wind_speed` | m/s | Vitesse du vent. |
| `wind_direction` | degrés | Direction du vent, 0360 (0 = Nord). |
## Diagnostic & firmware
### `error_flags` — bitfield système (1 octet)
Erreurs matérielles détectées par le capteur. `255` (0xFF) = firmware ancien, champ non supporté.
| Bit | Masque | Nom | Signification |
|-----|--------|---------------------|--------------------------------------------------------------|
| 0 | 0x01 | `RTC_DISCONNECTED` | Module RTC DS3231 non détecté (I2C). |
| 1 | 0x02 | `RTC_RESET` | RTC en date par défaut (année 2000). |
| 2 | 0x04 | `BME280_ERROR` | Capteur BME280 non détecté ou erreur. |
| 3 | 0x08 | `NPM_ERROR` | Capteur NextPM non détecté ou erreur. |
| 4 | 0x10 | `ENVEA_ERROR` | Capteurs Envea non détectés ou erreur. |
| 5 | 0x20 | `NOISE_ERROR` | Capteur bruit NSRT MK4 non détecté. |
| 6 | 0x40 | `MPPT_ERROR` | Chargeur solaire MPPT non détecté. |
| 7 | 0x80 | `WIND_ERROR` / `CO2_ERROR` | NebuleAir : vent non détecté. ModuleAir : CO₂ non détecté. |
Exemple : `error_flags = 5` → RTC déconnecté + BME280 en erreur.
### `npm_status` — bitfield NextPM (1 octet)
Registre d'état interne du capteur NextPM. Copie du byte `STATE` de la trame UART NextPM (voir [`sensors/nextpm.md`](../sensors/nextpm.md)). `255` = firmware ancien.
| Bit | Masque | Nom | Signification |
|-----|--------|------------------|------------------------------------------------------|
| 0 | 0x01 | `SLEEP_STATE` | Capteur en veille. |
| 1 | 0x02 | `DEGRADED_STATE` | Erreur mineure, précision réduite. |
| 2 | 0x04 | `NOT_READY` | Démarrage en cours (~15 s). |
| 3 | 0x08 | `HEAT_ERROR` | Humidité > 60 % pendant > 10 min. |
| 4 | 0x10 | `TRH_ERROR` | T/HR interne hors spécification. |
| 5 | 0x20 | `FAN_ERROR` | Ventilateur hors plage. |
| 6 | 0x40 | `MEMORY_ERROR` | Accès mémoire impossible. |
| 7 | 0x80 | `LASER_ERROR` | Aucune particule > 240 s, erreur laser. |
Exemple : `npm_status = 40``HEAT_ERROR` + `FAN_ERROR`.
### `device_status` — bitfield boîtier (1 octet)
État général du boîtier capteur. `255` = firmware ancien.
| Bit | Masque | Nom | Signification |
|-----|--------|------------------|--------------------------------------------------------------------|
| 0 | 0x01 | `SARA_REBOOTED` | Modem SARA a rebooté (hardware) au cycle précédent. |
| 1 | 0x02 | `WIFI_CONNECTED` | Device connecté en WiFi (mode atelier). |
| 2 | 0x04 | `HOTSPOT_ACTIVE` | Hotspot WiFi actif (mode configuration). |
| 3 | 0x08 | `GPS_NO_FIX` | Pas de position GPS valide. |
| 4 | 0x10 | `BATTERY_LOW` | Tension batterie sous seuil critique. |
| 5 | 0x20 | `DISK_FULL` | Espace disque critique (< 5 %). |
| 6 | 0x40 | `DB_ERROR` | Erreur d'accès à la base SQLite locale. |
| 7 | 0x80 | `BOOT_RECENT` | Device redémarré récemment (uptime < 5 min). |
Exemple : `device_status = 145` (0x91) → modem reboot + batterie faible + boot récent.
### Version firmware
| Champ | Type | Description |
|------------------|------|-----------------------------------------------|
| `version_major` | int | Numéro majeur (`X.y.z`). |
| `version_minor` | int | Numéro mineur (`x.Y.z`). |
| `version_patch` | int | Numéro de patch (`x.y.Z`). |
Reconstitution : `f"{version_major}.{version_minor}.{version_patch}"` → ex. `"1.2.3"`.
## Géolocalisation & contexte
| Champ | Type | Unité | Description |
|-------------|--------|---------|-------------------------------------------------------------------|
| `latitude` | number | degrés | Latitude GPS WGS84 (décimal). |
| `longitude` | number | degrés | Longitude GPS WGS84 (décimal). |
| `misc` | int | | Contexte de mesure, voir table ci-dessous. |
| `misc` | Contexte |
|--------|-----------------------------------------|
| 0 | Aucun |
| 1 | Mesure en intérieur |
| 2 | Mesure en extérieur |
| 3 | Mesure en voiture |
| 4 | Mesure en piéton |
| 5 | Mesure en vélo |
| 6 | Mesure en transport en commun |
Enum extensible — de nouvelles valeurs peuvent être ajoutées (7, 8, …) sans casser l'encodage 1 octet. Un consommateur qui rencontre une valeur inconnue doit la traiter comme *contexte non renseigné* (équivalent à `0`), pas rejeter la trame.
## Exemple complet (NebuleAir_Pro)
```json
{
"device_id": "4430353234313938",
"signal_quality": -22,
"signal_quality_unit": "-22 dB",
"command": 0,
"ISO_68": 0.8, "ISO_68_unit": "0,8 ugm3",
"ISO_54": 25.5, "ISO_54_unit": "25.5 °C",
"noise_cur_leq": 25.5, "noise_cur_leq_unit": "25,5 dB",
"noise_cur_level": 25.5, "noise_cur_unit": "25,5 dB",
"max_noise": 25.5, "max_noise_unit": "25,5 dB",
"npm_ch1": 255, "npm_ch1_unit": "255 nb",
"npm_ch2": 255, "npm_ch2_unit": "255 nb",
"npm_ch3": 255, "npm_ch3_unit": "255 nb",
"npm_ch4": 255, "npm_ch4_unit": "255 nb",
"npm_ch5": 255, "npm_ch5_unit": "255 nb",
"battery_voltage": 25.5, "battery_voltage_unit": "25,5 V",
"battery_current": 25.5, "battery_current_unit": "25,5 A",
"solar_voltage": 25.5, "solar_voltage_unit": "25,5 V",
"solar_power": 255, "solar_power_unit": "255 W",
"npm_temp": 25.5, "npm_temp_unit": "25,5 °C",
"npm_humidity": 25.5, "npm_humidity_unit": "25,5 %",
"wind_speed": 25.5, "wind_speed_unit": "25,5 m/s",
"wind_direction": 255, "wind_direction_unit": "255 degrees",
"charger_status": 255, "charger_status_unit": "255",
"error_flags": 0,
"npm_status": 0,
"device_status": 0,
"version_major": 1,
"version_minor": 2,
"version_patch": 3,
"latitude": 43.2964,
"longitude": 5.36978,
"misc": 2
}
```
## Notes d'intégration
### Côté capteur / firmware
- Envoyer uniquement les champs mesurés par ton modèle. Omettre les autres, ou les remplir à `-1` si leur présence est structurellement attendue par un parser amont (ex. descripteur Miotiq à taille fixe).
- Initialiser `error_flags`, `npm_status`, `device_status` à `0`. Les laisser à `0xFF` uniquement si tu ne sais pas renseigner (= firmware non migré), pour que le backend interprète bien « non disponible ».
### Côté backend
- Ignorer toute valeur `-1` (ne pas la stocker comme une mesure).
- Ignorer `error_flags`, `npm_status`, `device_status` si `== 255` — c'est un firmware ancien, l'absence de diagnostic n'est pas une alarme.
- Les `_unit` sont purement informatifs / de debug. La valeur métier est toujours le champ sans suffixe.
- `device_id` est **hex** dans le JSON ; convertir en ASCII (`bytes.fromhex(v).decode('ascii')`) pour afficher le numéro de série lisible.
- Lire `command` **avant** de persister la trame : si `command == 0x01` (ping test), logger la réception (timestamp, `device_id`, `signal_quality`) sans archiver les mesures. Une trame `command` absent ou `0x00` est une trame de données normale.
## Historique
| Date | Révision | Changement |
|------------|----------|---------------------------------------------------------------------------|
| 2026-04-23 | v1 | Version initiale inventée (schéma imbriqué avec `token`/`ts`/`measurements`) — remplacée. |
| 2026-04-23 | v2 | **Format officiel AirCarto 2026** : schéma plat, bitfields détaillés, compat firmware ancien. |
| 2026-04-27 | v3 | Champ `version` (offset 9 du binaire, hardcodé `0x01` côté firmware) renommé `command` et réaffecté à un type de trame : `0x00` = données, `0x01` = ping test. Ajout de la section « Commande / type de trame ». Versioning protocole reste sur `version_major/minor/patch`. |