Aller au contenu
Sylvain Joffraud l4w
Retour

Anti-spam notifs IoT : pattern épisode + ack + ré-armement

Setup classique : un capteur ou un device IoT déclenche une alerte (intrusion, géofence, capteur out-of-bound, service down…). Première version naïve : on envoie un SMS / push à chaque évènement → spam de notifs si la condition perdure (30 min de problème = 30 notifs).

Deuxième version : un cooldown entre 2 notifs (ex. 5 min). Mieux, mais quand tu as VU la première notif, tu reçois quand même la suivante 5 min plus tard. Friction inutile.

La bonne version : machine d’état « épisode » avec acquittement explicite.

Le modèle

Un épisode = une période continue où la condition d’alerte est vraie. Il a :

Au plus un seul épisode ouvert par device (unique index partiel WHERE ended_at IS NULL).

Le flow

  1. Condition d’alerte devient vraie → ouvre un épisode (INSERT), envoie 1ère notif
  2. Tant que l’épisode n’est pas acquitté ET que la condition reste vraie → re-notif tous les N secondes
  3. Ack via URL : la notif contient https://app/ack/<token> → click → UPDATE ... SET ack_at=NOW(), ack_method='url'
  4. Ack via bouton dans l’UI : POST endpoint → UPDATE ... ack_method='button'
  5. Une fois ack, plus de re-notif pour cet épisode
  6. Condition redevient fausse → ferme l’épisode (ended_at=NOW())
  7. Si la condition redevient vraie → nouvel épisode avec nouveau token → re-notification

Plus un bouton « Réarmer » dans l’UI : UPDATE ... SET ack_at=NULL, ack_method=NULL — pour les ack par erreur.

Pourquoi un token random PAR ÉPISODE

Risque pré-fetch des URL dans SMS

Certains clients (iMessage, WhatsApp, mobile preview) pré-fetchent les URL affichées en preview → risque d’ack accidentel.

3 options selon la sensibilité :

Pour un projet perso avec SMS vers son propre numéro, GET immédiat acceptable. Pour multi-utilisateurs ou notif sensible (admin de prod), la page de confirmation s’impose.

Schéma SQL minimal

CREATE TABLE outings (
  id            BIGSERIAL PRIMARY KEY,
  device_id     TEXT NOT NULL,
  started_at    TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  ended_at      TIMESTAMPTZ,
  ack_token     TEXT NOT NULL UNIQUE,
  ack_at        TIMESTAMPTZ,
  ack_method    TEXT,
  last_alert_at TIMESTAMPTZ,
  alert_count   INT NOT NULL DEFAULT 0
);
CREATE UNIQUE INDEX outings_one_open
  ON outings (device_id) WHERE ended_at IS NULL;

L’unique index partiel garantit qu’on ne peut jamais avoir 2 épisodes ouverts pour le même device — si on tente, l’INSERT échoue. Plus sûr qu’un check applicatif (pas de race condition).

Lessons learned

#alerting #notification #iot #patterns #state-machine #sql #postgresql


Partager cet article :

Article précédent
Tracker GPS : positions « fantôme » sans fix (sat absent)
Article suivant
1NCE Flat 10 ans : 500 Mo TOTAL sur la décennie, pas annuel