Simuler une mesure IoT¶
Objectif : alimenter la base d'événements sans capteur ni broker, pour
travailler en local.
Ce que vous allez apprendre : composer une mesure, la valider contre le
contrat IoT (build_payload + parse_message) puis l'insérer dans
iot_events via IotEventRepository.insert. C'est exactement la validation que
le subscriber MQTT applique en production : on emprunte simplement un autre
chemin d'entrée, sans broker.
Premier palier du niveau intermédiaire de la progression IoT, après le
niveau débutant.
Ce que ce starter montre¶
- un formulaire qui compose une mesure (site, capteur, type, valeur, unité) ;
- la construction d'un payload conforme au contrat (
build_payload) ; - la validation
parse_message(topic + payload →Measurement) ; - l'insertion via
IotEventRepository.insert; - la boucle complète : injecter puis relire la liste, en local.
La table iot_events est créée par la migration livrée avec le starter.
Classes Forge utilisées¶
| Classe / fonction | Rôle dans ce starter | Référence |
|---|---|---|
build_topic / build_payload |
Composer un topic et un payload conformes. | Forge IoT : simulateur |
parse_message |
Valider topic + payload → Measurement. |
Forge IoT : contrat |
IotEventRepository.insert |
Écrire la mesure validée dans iot_events. |
Forge IoT : stockage |
BaseController.csrf_token / redirect |
Protéger le POST, rediriger (PRG). | BaseController |
Tester¶
Ouvrez https://localhost:8000/iot-simulate, ajustez les champs et cliquez
Injecter la mesure : elle apparaît dans la liste des derniers événements. Vous
venez d'alimenter iot_events sans broker.
Le contrôleur¶
Créez le fichier ci-dessous, complet et copiable tel quel.
# mvc/controllers/iot_simulate_controller.py
import json
from core.http.request import Request
from core.http.response import Response
from core.mvc.controller.base_controller import BaseController
from forge_mvc_iot.cli.simulate import build_payload, build_topic, utc_timestamp
from forge_mvc_iot.mqtt.contract import ContractError, parse_message
from forge_mvc_iot.storage import IotEventRepository
class IotSimulateController(BaseController):
"""Starter pédagogique : injecter une mesure simulée, validée, sans broker."""
@staticmethod
def index(request: Request) -> Response:
return IotSimulateController._page(request)
@staticmethod
def simulate(request: Request) -> Response:
site = (request.form("site") or "atelier").strip()
device_id = (request.form("device_id") or "capteur-1").strip()
kind = (request.form("kind") or "temperature").strip()
unit = (request.form("unit") or "C").strip()
raw_value = (request.form("value") or "").strip()
try:
value = float(raw_value)
except ValueError:
return IotSimulateController._page(
request, error="La valeur doit être un nombre."
)
try:
topic = build_topic(site, device_id)
payload = build_payload(
kind=kind, value=value, unit=unit, timestamp=utc_timestamp()
)
measurement = parse_message(topic, json.dumps(payload))
IotEventRepository().insert(measurement)
except ContractError as exc:
return IotSimulateController._page(request, error=str(exc))
return BaseController.redirect("/iot-simulate")
@staticmethod
def _page(request: Request, error: str | None = None) -> Response:
try:
events = IotEventRepository().list_recent(limit=20)
except Exception:
events = []
context = {
"events": events,
"csrf_token": BaseController.csrf_token(request),
}
if error:
context["error"] = error
return BaseController.render(
"iot_simulate/index.html", context=context, request=request
)
Comprendre ce code¶
build_payload(...)produit un payload conforme (champskind,value,
unit,timestamp).parse_messagele valide et renvoie unMeasurement
typé ; uneContractErrorest levée si quelque chose ne respecte pas le
contrat.IotEventRepository().insert(measurement)écrit la mesure validée. C'est le
même repository qu'en lecture.- En production, ce
Measurementviendrait du broker via le subscriber ; ici, il
vient du formulaire. Le reste du chemin est identique.
La vue¶
Créez le gabarit ci-dessous : il porte le formulaire et la liste des derniers
événements.
<!-- mvc/views/iot_simulate/index.html -->
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Simuler une mesure IoT — Forge</title>
</head>
<body>
<h1>Simuler une mesure IoT</h1>
{% if error %}
<p data-level="error"><strong>{{ error }}</strong></p>
{% endif %}
<form method="post" action="/iot-simulate">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<label>Site <input type="text" name="site" value="atelier"></label>
<label>Capteur <input type="text" name="device_id" value="capteur-1"></label>
<label>Type <input type="text" name="kind" value="temperature"></label>
<label>Valeur <input type="text" name="value" value="21.5"></label>
<label>Unité <input type="text" name="unit" value="C"></label>
<button type="submit">Injecter la mesure</button>
</form>
<h2>Derniers événements</h2>
{% if events %}
<ul>
{% for e in events %}
<li>{{ e.site }}/{{ e.device_id }} — {{ e.kind }} = {{ e.value }} {{ e.unit }}</li>
{% endfor %}
</ul>
{% else %}
<p>Aucun événement pour l'instant.</p>
{% endif %}
</body>
</html>
La route¶
Déclarez les deux routes (GET pour la page, POST pour l'injection) dans
mvc/routes.py, à l'intérieur du groupe public.
# mvc/routes.py
from mvc.controllers.iot_simulate_controller import IotSimulateController
with router.group("", public=True) as public:
public.add("GET", "/iot-simulate", IotSimulateController.index, name="iot_simulate_index")
public.add("POST", "/iot-simulate", IotSimulateController.simulate, name="iot_simulate_store")
La migration¶
La simulation a besoin de la table iot_events. Créez le fichier de migration
ci-dessous (le nom commence par un horodatage), puis appliquez-le avec
forge db:init.
-- mvc/migrations/20260601170000_create_iot_events.sql
CREATE TABLE IF NOT EXISTS iot_events (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
site VARCHAR(64) NOT NULL,
device_id VARCHAR(64) NOT NULL,
kind VARCHAR(64) NOT NULL,
value DOUBLE NOT NULL,
unit VARCHAR(32) NOT NULL,
timestamp VARCHAR(40) NOT NULL,
metadata_json TEXT NULL,
received_at DATETIME(6) NOT NULL,
PRIMARY KEY (id),
INDEX idx_iot_events_site_device (site, device_id),
INDEX idx_iot_events_received_at (received_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS rend la migration idempotente : elle est sûre même
si un autre palier a déjà créé la table.
À retenir¶
- On peut alimenter
iot_eventssans broker, via le contrat IoT. parse_messagevalide la mesure exactement comme en production.IotEventRepository.insertécrit ;list_recentrelit : la boucle est
bouclée en local.
Après ce starter¶
Vous savez alimenter les données en local. La suite : exposer l'API HTTP JSON
officielle.