API HTTP Forge IoT¶
Statut : API JSON de lecture des événements IoT, avec une protection optionnelle par Bearer token (voir Protection par Bearer token). L'API d'ingestion (POST) et le dashboard restent hors périmètre — voir Architecture Forge IoT.
Routes¶
| Méthode | URL | Repository |
|---|---|---|
GET |
/api/iot/events |
IotEventRepository.list_recent |
GET |
/api/iot/events/{site}/{device_id} |
IotEventRepository.find_by_device |
GET |
/api/iot/devices/{site}/{device_id}/count |
IotEventRepository.count_by_device |
Toutes les routes sont :
public=True— ouvertes par défaut (parcours local/pédagogique) ; une protection optionnelle par Bearer token s'active viaFORGE_IOT_API_TOKEN(voir Protection par Bearer token) ;csrf=False— méthodes GET, sans état modifié ;api=True— marquées comme routes API par Forge.
Branchement explicite¶
Le module IoT reste opt-in : Forge Core n'enregistre rien
automatiquement. L'application déclare les routes elle-même depuis son
mvc/routes.py :
# mvc/routes.py
from forge_mvc_iot import register_iot_routes
def setup_routes(router):
register_iot_routes(router)
# … vos autres routes …
register_iot_routes accepte un argument optionnel repository= pour
injecter une instance préconstruite (utile pour les tests ou pour
partager un repository entre plusieurs composants) :
from forge_mvc_iot.storage import IotEventRepository
from forge_mvc_iot import register_iot_routes
repo = IotEventRepository()
register_iot_routes(router, repository=repo)
Si repository n'est pas fourni, un IotEventRepository() par défaut
est instancié (utilise core.database.db comme adapter).
GET /api/iot/events¶
Retourne les N derniers événements toutes sources confondues,
ordre received_at DESC.
Paramètres¶
| Paramètre | Défaut | Plage | Comportement hors plage |
|---|---|---|---|
?limit= |
100 |
1..1000 |
400 invalid_limit |
Exemple¶
{
"events": [
{
"id": 1,
"site": "atelier",
"device_id": "esp32-001",
"kind": "temperature",
"value": 22.4,
"unit": "°C",
"timestamp": "2026-05-28T10:00:00Z",
"metadata": {"room": "atelier"},
"received_at": "2026-05-28T10:00:05Z"
},
{
"id": 2,
"site": "atelier",
"device_id": "esp32-001",
"kind": "humidity",
"value": 47,
"unit": "%",
"timestamp": "2026-05-28T10:00:05Z",
"metadata": null,
"received_at": "2026-05-28T10:00:10Z"
}
]
}
GET /api/iot/events/{site}/{device_id}¶
Retourne les événements d'un device précis, ordre received_at DESC.
Format de réponse identique à /api/iot/events (clé events).
GET /api/iot/devices/{site}/{device_id}/count¶
Retourne le nombre d'événements enregistrés pour un device.
Sérialisation received_at¶
Côté repository, received_at est un datetime Python. L'API le
convertit en chaîne ISO 8601 UTC avec suffixe Z :
| Entrée (repository) | Sortie JSON |
|---|---|
datetime(2026, 5, 28, 10, 0, 5, tzinfo=UTC) |
"2026-05-28T10:00:05Z" |
datetime(2026, 5, 28, 12, 0, 5, tzinfo=+02:00) (Paris) |
"2026-05-28T10:00:05Z" (converti en UTC) |
datetime(2026, 5, 28, 10, 0, 5) (naïf) |
"2026-05-28T10:00:05Z" (assumé UTC) |
Tous les autres fuseaux sont convertis en UTC avant sérialisation —
la sortie n'expose jamais un offset autre que Z. C'est cohérent avec
le contrat MQTT, qui exige déjà Z côté payload.
metadata et metadata_json¶
metadata_json est un détail interne de stockage. Il n'apparaît
jamais dans les réponses HTTP :
metadata(objet ounull) est la seule clé exposée ;- la conversion JSON ↔ dict est déjà faite côté repository ;
- même si un consommateur indiscipliné fait fuiter
metadata_jsondans son dict, le sérialiseur HTTP le supprime explicitement (vérifié partest_metadata_json_key_never_present).
Format des erreurs¶
Limit invalide — 400 Bad Request¶
Cas couverts :
- non convertible en
int(?limit=abc) ; - nul ou négatif (
?limit=0,?limit=-1) ; - au-dessus de
MAX_LIMIT(?limit=1001).
Le repository n'est pas appelé — la validation est purement côté contrôleur.
Erreur DB — 500 Internal Server Error¶
Réponse sobre : aucun message SQL, aucun stacktrace, aucun détail
qui pourrait fuiter de l'information. Le détail est logué côté serveur
sur le logger forge_mvc_iot.http (niveau ERROR via
logger.exception).
Non autorisé — 401 Unauthorized¶
Renvoyé quand un Bearer token est configuré (voir ci-dessous) mais que la requête n'en fournit pas, fournit un mauvais schéma, ou un mauvais token. La réponse reste sobre : elle ne précise pas la cause et ne renvoie jamais le token.
Protection par Bearer token¶
Par défaut, l'API est ouverte — pratique en local et pour les
parcours pédagogiques. Pour un projet exposé sur le réseau, définis
FORGE_IOT_API_TOKEN : les trois routes exigent alors un en-tête
Authorization: Bearer <token>.
export FORGE_IOT_API_TOKEN="change-me"
# Sans header → 401
curl http://localhost:8000/api/iot/events
# Avec le bon token → réponse normale
curl -H "Authorization: Bearer change-me" \
http://localhost:8000/api/iot/events
Règles :
FORGE_IOT_API_TOKENabsent ou vide → API ouverte (aucun header requis) ;FORGE_IOT_API_TOKENdéfini →Authorization: Bearer <token>obligatoire ; toute absence, mauvais schéma ou mauvais token →401;- la comparaison utilise
secrets.compare_digest(temps constant) ; - le token est masqué dans
repr(IotConfig)et n'apparaît jamais dans une réponse JSON.
Pour un test local, le token peut rester absent. Pour un projet exposé sur le réseau, il faut définir
FORGE_IOT_API_TOKEN.
Cette protection vit dans le module forge-mvc-iot (http.py), jamais
dans Forge Core. Hors périmètre : JWT, OAuth, session, RBAC, refresh
token, rotation, stockage DB du token, TLS.
Utilisation directe du contrôleur¶
Pour un usage avancé (composer une route personnalisée, ajouter un
middleware spécifique), IotHttpController est exposé :
from forge_mvc_iot.http import IotHttpController
from forge_mvc_iot.storage import IotEventRepository
controller = IotHttpController(IotEventRepository())
# Ou avec protection par token :
# IotHttpController(IotEventRepository(), api_token="change-me")
router.add(
"GET", "/custom/events", controller.list_events,
name="custom_events_list",
public=True, csrf=False, api=True,
)
Hors périmètre¶
- l'authentification se limite au Bearer token statique ci-dessus : pas de JWT, OAuth, session, RBAC ni rotation ;
- pas de POST/ingestion HTTP — l'ingestion se fait par MQTT (subscriber) ;
- pas de pagination par offset (
?offset=) ; - pas de filtres temporels (
?since=,?until=) ; - pas d'agrégation (
avg,min,maxsur une fenêtre) ; - pas de dashboard HTML, pas d'intégration Forge Design ;
- pas de downlink Forge → capteur.
Ces points feront chacun l'objet d'un ticket dédié.