docs: planifier l'API REST Home Assistant
Co-authored-by: OpenAI/GPT-5.6-Luna-Pro <vibecoder@antoineve.me>
This commit is contained in:
266
docs/plans/2026-08-13-api-rest-home-assistant.md
Normal file
266
docs/plans/2026-08-13-api-rest-home-assistant.md
Normal file
@@ -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.<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 :
|
||||
|
||||
```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.
|
||||
Reference in New Issue
Block a user