Files
tableau-de-bord/docs/plans/2026-08-13-api-rest-home-assistant.md
2026-08-13 15:57:32 +02:00

11 KiB

Plan d'implémentation : API REST Home Assistant

Objectif

Ajouter une API REST privée permettant à Home Assistant d'enregistrer automatiquement les arrivées et les départs dans la zone lieu de travail, via rest_command et un tracker person.

L'API doit créer automatiquement une journée de type WORK, utiliser par défaut le profil de trajet moteur_seul et le véhicule citadine (Twingo ZE dans le fichier de configuration), tout en laissant la modification complète de la journée dans l'interface Web.

Le fuseau métier est Europe/Paris. Plusieurs plages horaires dans une même journée doivent être supportées, comme dans l'interface existante.

Décisions fonctionnelles

  • L'arrivée crée la journée si elle n'existe pas, avec day_type = "WORK".
  • Le profil de trajet par défaut est moteur_seul.
  • Le véhicule par défaut est citadine.
  • Les journées FORMATION et GARDE restent compatibles avec le lieu de travail ; l'API ne bloque donc pas ces types lorsqu'une journée existe déjà.
  • Plusieurs couples arrivée/départ sont autorisés dans la même journée.
  • Une arrivée ouverte ne doit pas être stockée dans TimeSlot, car le modèle actuel exige un end_time et WorkEntry.total_minutes() suppose une plage complète.
  • Une table d'événements de présence conserve les arrivées ouvertes et les événements traités ; un TimeSlot est créé uniquement lorsqu'un départ complète une arrivée.
  • Les utilisateurs continuent à corriger les journées et le mode de transport depuis l'interface Web existante.
  • L'import CSV direct en base n'est pas modifié dans cette livraison.

Contrat HTTP cible

Endpoint

POST /api/v1/workplace-presence

Requête

{
  "event": "arrival",
  "occurred_at": "2026-08-13T08:23:10+02:00",
  "idempotency_key": "person.telephone.arrival.20260813T082310+0200"
}

event vaut arrival ou departure. occurred_at est un timestamp ISO 8601 avec offset obligatoire. La date et l'heure enregistrées dans le modèle sont calculées dans Europe/Paris.

La clé d'idempotence est obligatoire et doit être limitée en taille. Elle est également acceptée dans X-Idempotency-Key; l'implémentation doit refuser une valeur absente ou contradictoire entre l'en-tête et le JSON.

Réponses

  • 201 Created pour une arrivée qui crée un nouvel événement.
  • 200 OK pour un départ qui complète une plage ou pour une requête idempotente déjà traitée.
  • 400 ou 422 pour un JSON ou une valeur métier invalide.
  • 401 pour un token Bearer absent ou invalide.
  • 409 pour un départ sans arrivée ouverte ou une transition incohérente.
  • 415 si le contenu n'est pas application/json.
  • 413 si le corps dépasse la limite configurée.
  • 429 si la limite de débit est dépassée.

Toutes les erreurs sont des objets JSON homogènes, sans traceback ni détail interne. Les réponses API indiquent Cache-Control: no-store.

Étapes d'implémentation

1. Formaliser la configuration

Ajouter une section dédiée dans config.toml :

[home_assistant]
timezone = "Europe/Paris"
default_day_type = "WORK"
default_journey_profile_id = "moteur_seul"
default_motor_vehicle_id = "citadine"

Étendre app/config_loader.py avec un accès validé à cette configuration. Vérifier au démarrage que le type de journée, le profil et le véhicule existent et que le véhicule par défaut est bien un véhicule à moteur. Documenter que le token API ne doit pas être placé dans TOML, mais fourni par WORKLOG_API_TOKEN.

2. Ajouter la persistance des événements de présence

Créer un modèle, par exemple WorkplacePresenceEvent, contenant au minimum :

  • un identifiant primaire ;
  • une clé d'idempotence unique ;
  • le type (arrival ou departure) ;
  • le timestamp reçu et le timestamp converti dans Europe/Paris ;
  • la date locale ;
  • l'identifiant de WorkEntry ;
  • l'identifiant de TimeSlot lorsque le départ a complété une plage ;
  • les dates de création et de traitement.

Le modèle doit permettre de retrouver une arrivée ouverte pour une date et de conserver la réponse logique d'une requête rejouée. Ajouter les index nécessaires sur la date locale, l'entrée et la clé unique.

Adapter _migrate_db dans app/__init__.py pour créer la nouvelle table dans les installations existantes, en vérifiant d'abord la présence de la base et des tables. Ajouter aussi la couverture de la base vide. Respecter la contrainte du projet : il n'y a pas d'Alembic et les changements de schéma sont manuels.

3. Factoriser le service métier

Créer un service sans dépendance Flask, dans app/business/, chargé de :

  • convertir et valider un timestamp ISO 8601 ;
  • déterminer la date et l'heure locales dans Europe/Paris ;
  • créer ou retrouver une WorkEntry avec les valeurs par défaut ;
  • ouvrir une présence à l'arrivée ;
  • retrouver l'arrivée ouverte la plus ancienne ou la plus récente selon la règle retenue et la fermer au départ ;
  • créer un TimeSlot complet pour chaque couple ;
  • accepter plusieurs plages dans la même journée ;
  • refuser un départ sans arrivée ouverte ;
  • appliquer l'idempotence dans la même transaction SQLAlchemy.

La règle de rattachement doit être explicite pour les événements autour de minuit. La date locale de l'arrivée ouvre la plage ; le départ doit être rattaché à cette arrivée ouverte, même si son heure locale est le lendemain. Vérifier que cette plage reste compatible avec le calcul existant du passage de minuit.

Réutiliser autant que possible les validations communes avec entries.py. Ne pas faire dépendre le service des messages Flash, des redirections ou des templates.

4. Implémenter le blueprint API

Créer app/routes/api.py ou app/routes/api/ et enregistrer le blueprint dans la factory Flask sous /api/v1.

Implémenter :

  • validation stricte du content type et du JSON ;
  • authentification Authorization: Bearer ... ;
  • comparaison du secret en temps constant ;
  • contrôle de la clé d'idempotence ;
  • appel du service métier ;
  • sérialisation JSON stable ;
  • traduction des erreurs métier en statuts HTTP ;
  • gestion générique des erreurs inattendues sans fuite d'informations.

Limiter l'API à POST pour cette première version. Ajouter 405 pour les autres méthodes et ne pas activer CORS, qui n'est pas nécessaire pour Home Assistant.

Configurer une taille maximale de requête adaptée à ce payload et prévoir un rate limiting simple. Si aucune dépendance n'est souhaitable, une protection minimale par token et fenêtre temporelle peut être implémentée ; sinon sélectionner une dépendance légère et maintenue après vérification des contraintes de production.

5. Préserver le comportement Web

Vérifier que l'interface existante continue à afficher et modifier les TimeSlot complets créés par l'API. Une journée créée par une arrivée doit être éditable même avant le départ, avec zéro plage complète à ce stade.

Vérifier notamment que l'édition Web peut :

  • changer WORK en FORMATION ou GARDE ;
  • modifier le profil de trajet ;
  • modifier le véhicule, notamment abandonner la Twingo par défaut ;
  • ajouter, supprimer ou corriger plusieurs plages.

6. Documenter l'API séparément

Créer docs/api.md avec :

  • le but et le périmètre de l'API ;
  • l'URL, le contrat JSON et l'authentification ;
  • les exemples de réponses 200, 201, 401, 409 et erreurs de validation ;
  • la sémantique d'idempotence ;
  • la gestion du fuseau Europe/Paris ;
  • le comportement autour de minuit ;
  • les règles de déploiement HTTPS/HAProxy ;
  • les commandes curl de test, sans secret en clair dans la documentation.

Ne jamais écrire de valeur réelle de WORKLOG_API_TOKEN dans ce fichier.

7. Documenter Home Assistant

Créer docs/home-assistant.md ou une section dédiée dans docs/api.md contenant un exemple complet avec le tracker person :

  • stockage du token dans secrets.yaml ;
  • définition de rest_command en JSON ;
  • automatisation d'arrivée lorsque person.<nom> passe à lieu_de_travail ;
  • automatisation de départ lorsque l'état quitte cette zone ;
  • génération d'une clé d'idempotence déterministe ;
  • contrôle de response_variable et traitement des statuts non 200/201 ;
  • avertissement sur les traces et l'accès administrateur Home Assistant aux secrets.

L'exemple doit conserver verify_ssl: true et utiliser l'en-tête Bearer.

8. Tester et vérifier

Ajouter des tests de routes et de service couvrant au minimum :

  • arrivée authentifiée créant une journée WORK avec moteur_seul et citadine ;
  • départ complétant la première plage ;
  • deuxième arrivée et deuxième départ le même jour ;
  • journée FORMATION ou GARDE existante ;
  • départ sans arrivée ouverte ;
  • timestamp avec offset et conversion Europe/Paris ;
  • passage de minuit ;
  • clé rejouée sans duplication ;
  • clé réutilisée avec un contenu différent ;
  • token absent ou invalide ;
  • content type, JSON, champs et longueurs invalides ;
  • limite de taille et rate limiting ;
  • absence de secret ou de traceback dans les réponses ;
  • non-régression des tests HTML existants.

Exécuter ensuite :

.venv/bin/python -m pytest
.venv/bin/ruff check .
.venv/bin/ruff format --check .

Tester manuellement depuis Home Assistant avec rest_command et vérifier la réponse dans les traces d'automatisation, puis tester une répétition du même événement.

Hors périmètre et TODO ultérieure

Import CSV via l'API

Ne pas modifier scripts/import_csv.py dans cette livraison. Prévoir une TODO ultérieure pour faire passer l'import CSV par l'API plutôt que par des écritures directes en base.

Cette évolution nécessitera probablement de nouveaux endpoints ou un contrat d'import en lot, par exemple :

POST /api/v1/work-entries
POST /api/v1/work-entries/bulk

Elle devra définir les règles de transaction, le comportement en cas de doublon, les erreurs par ligne, l'idempotence d'un lot et une authentification adaptée à un client local. Elle devra aussi réutiliser le service métier commun introduit par la présente API.

Sources de référence

La recherche a été réalisée avec des moyens Web alternatifs ; FireCrawl local n'était pas disponible au moment de la préparation du plan.