diff --git a/docs/plans/2026-08-13-api-rest-home-assistant.md b/docs/plans/2026-08-13-api-rest-home-assistant.md new file mode 100644 index 0000000..901193b --- /dev/null +++ b/docs/plans/2026-08-13-api-rest-home-assistant.md @@ -0,0 +1,266 @@ +# 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 + +```text +POST /api/v1/workplace-presence +``` + +### Requête + +```json +{ + "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` : + +```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.` 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 : + +```bash +.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 : + +```text +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 + +- Home Assistant, `rest_command` : https://www.home-assistant.io/integrations/rest_command/ +- Home Assistant, secrets : https://www.home-assistant.io/docs/configuration/secrets/ +- OWASP REST Security Cheat Sheet : https://cheatsheetseries.owasp.org/cheatsheets/REST_Security_Cheat_Sheet.html +- OWASP API Security Top 10 : https://owasp.org/API-Security/editions/2023/en/0x11-t10/ +- Flask, Web Security Considerations : https://flask.palletsprojects.com/en/stable/web-security/ + +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.