Files
tableau-de-bord/app/config_loader.py
Antoine Van Elstraete 85e6e502e5 feat(config): ajouter la configuration Home Assistant (étape 1)
Ajoute la section [home_assistant] au TOML et sa validation au chargement
via get_home_assistant_config(). La section est facultative pour préserver
la compatibilité avec les configurations existantes, mais si elle est
présente elle doit être complète et référencer un type de journée, un
trajet et un véhicule à moteur existants, sous peine de refuser le
démarrage de l'application. La configuration validée est exposée dans
app.config['HOME_ASSISTANT'].

Co-authored-by: OpenAI/GPT-5.6-Luna-Pro <vibecoder@antoineve.me>
2026-08-13 16:13:18 +02:00

205 lines
7.9 KiB
Python

"""Module de chargement et d'accès à la configuration TOML de l'application.
Ce module sert d'interface entre l'application Flask et le fichier `config.toml`.
Toute la configuration des véhicules, des trajets et du barème kilométrique y est stockée
et chargée au démarrage dans `app.config["TOML"]`.
Contrat TOML :
1. Véhicules (`[vehicles]`) :
- Chaque véhicule possède un identifiant unique (clé).
- Attributs : `name` (nom d'affichage), `type` ("moteur" ou "velo"), `fuel` ("electric", "essence", etc.),
`co2_per_km` (émissions de CO2 en grammes par km), et optionnellement `cv` (puissance fiscale pour le barème kilométrique).
2. Trajets (`[journeys]`) :
- Profils de trajets prédéfinis (ex: "moteur_seul").
- Attributs : `name` (nom d'affichage), `distances` (dictionnaire associant un type de véhicule à une distance en km).
3. Barème kilométrique (`[bareme_kilometrique.YYYY]`) :
- Organisé par année (ex: "2026") puis par puissance fiscale (`cv_3`, `cv_4`, `cv_5`, `cv_6`, `cv_7plus`).
- Chaque catégorie contient une liste de `tranches` définissant les formules de calcul des frais réels.
- Une tranche possède : `km_max` (limite supérieure de la tranche, `km_max = 0` signifie "pas de limite supérieure"),
`taux` (coefficient multiplicateur par km) et `forfait` (montant forfaitaire à ajouter).
4. Types de jours sans trajet :
- Certains types de journées (Télétravail, Maladie, Congé, RTT, Férié) n'impliquent aucun déplacement physique.
"""
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from flask import current_app
_HOME_ASSISTANT_REQUIRED_KEYS = {
"timezone",
"default_day_type",
"default_journey_profile_id",
"default_motor_vehicle_id",
}
_VALID_DAY_TYPES = {
"WORK",
"TT",
"GARDE",
"ASTREINTE",
"FORMATION",
"RTT",
"CONGE",
"MALADE",
"FERIE",
}
def get_vehicles():
"""Récupère l'ensemble des véhicules configurés dans le fichier TOML.
Retourne:
dict: Un dictionnaire des véhicules où la clé est l'identifiant du véhicule
et la valeur est un dictionnaire contenant ses propriétés (name, type, fuel, co2_per_km, cv).
Retourne un dictionnaire vide si aucune configuration n'est chargée.
"""
return current_app.config.get("TOML", {}).get("vehicles", {})
def get_motor_vehicles():
"""Filtre et retourne uniquement les véhicules de type 'moteur'.
Cette fonction exclut les véhicules alternatifs (comme les vélos) pour ne conserver
que ceux qui possèdent une puissance fiscale (CV) et sont éligibles au barème kilométrique.
Retourne:
dict: Un dictionnaire contenant uniquement les véhicules dont le type est 'moteur'.
"""
return {k: v for k, v in get_vehicles().items() if v.get("type") == "moteur"}
def get_journeys():
"""Récupère l'ensemble des profils de trajets configurés dans le fichier TOML.
Retourne:
dict: Un dictionnaire des trajets où la clé est l'identifiant du trajet
et la valeur est un dictionnaire contenant ses propriétés (name, distances).
Retourne un dictionnaire vide si aucune configuration n'est chargée.
"""
return current_app.config.get("TOML", {}).get("journeys", {})
def get_home_assistant_config() -> dict[str, str] | None:
"""Retourne la configuration Home Assistant, après validation.
L'absence de section désactive la future intégration et reste compatible avec
les anciens fichiers TOML. Une section présente doit en revanche être
complète et cohérente avec les véhicules, trajets et types de journées
connus de l'application.
"""
config = current_app.config.get("TOML", {}).get("home_assistant")
if config is None:
return None
if not isinstance(config, dict):
raise ValueError("Configuration [home_assistant] invalide : la section doit être une table")
missing = _HOME_ASSISTANT_REQUIRED_KEYS - config.keys()
if missing:
missing_keys = ", ".join(sorted(missing))
raise ValueError(
f"Configuration [home_assistant] incomplète : clé(s) manquante(s) {missing_keys}"
)
if any(
not isinstance(config[key], str) or not config[key] for key in _HOME_ASSISTANT_REQUIRED_KEYS
):
raise ValueError(
"Configuration [home_assistant] invalide : toutes les valeurs doivent être des chaînes non vides"
)
timezone = config["timezone"]
try:
ZoneInfo(timezone)
except (ZoneInfoNotFoundError, ValueError) as exc:
raise ValueError(
f"Configuration [home_assistant] invalide : fuseau horaire inconnu {timezone!r}"
) from exc
day_type = config["default_day_type"]
if day_type not in _VALID_DAY_TYPES:
raise ValueError(
f"Configuration [home_assistant] invalide : type de journée inconnu {day_type!r}"
)
journey_id = config["default_journey_profile_id"]
if journey_id not in get_journeys():
raise ValueError(f"Configuration [home_assistant] invalide : trajet inconnu {journey_id!r}")
vehicle_id = config["default_motor_vehicle_id"]
vehicle = get_vehicles().get(vehicle_id)
if vehicle is None:
raise ValueError(
f"Configuration [home_assistant] invalide : véhicule inconnu {vehicle_id!r}"
)
if vehicle.get("type") != "moteur":
raise ValueError(
f"Configuration [home_assistant] invalide : le véhicule {vehicle_id!r} n'est pas un véhicule moteur"
)
return {key: config[key] for key in _HOME_ASSISTANT_REQUIRED_KEYS}
def journey_has_motor(journey_profile_id: str | None) -> bool:
"""Vérifie si un profil de trajet donné inclut une distance pour véhicule à moteur.
Cette validation permet de déterminer si l'utilisateur doit sélectionner un véhicule
à moteur lors de la saisie d'une journée de travail avec ce trajet.
Paramètres:
journey_profile_id (str | None): L'identifiant du profil de trajet à vérifier.
Retourne:
bool: True si le trajet existe et définit une distance pour la clé 'moteur',
False sinon ou si l'identifiant est nul.
"""
if not journey_profile_id:
return False
journeys = get_journeys()
profile = journeys.get(journey_profile_id, {})
return "moteur" in profile.get("distances", {})
def get_bareme(year: int, cv: int) -> list[dict]:
"""Récupère les tranches du barème kilométrique pour une année et une puissance fiscale données.
Le barème kilométrique officiel est structuré en tranches de distances annuelles.
Cette fonction sélectionne la bonne catégorie de puissance fiscale (CV) :
- cv <= 3 -> 'cv_3'
- cv == 4 -> 'cv_4'
- cv == 5 -> 'cv_5'
- cv == 6 -> 'cv_6'
- cv >= 7 -> 'cv_7plus'
Paramètres:
year (int): L'année civile concernée par le calcul.
cv (int): La puissance fiscale du véhicule en chevaux fiscaux.
Retourne:
list[dict]: Une liste de dictionnaires représentant les tranches applicables.
Chaque tranche contient 'km_max' (0 si pas de limite), 'taux' et 'forfait'.
"""
bareme = current_app.config.get("TOML", {}).get("bareme_kilometrique", {})
year_data = bareme.get(str(year), {})
if cv <= 3:
key = "cv_3"
elif cv == 4:
key = "cv_4"
elif cv == 5:
key = "cv_5"
elif cv == 6:
key = "cv_6"
else:
key = "cv_7plus"
return year_data.get(key, {}).get("tranches", [])
def day_types_without_journey():
"""Retourne l'ensemble des types de jours qui n'impliquent aucun trajet physique.
Ces types de jours (Télétravail, Maladie, Congé, RTT, Férié) sont exemptés de la saisie
de trajets ou de véhicules, car le travail s'effectue à distance ou l'employé est absent.
Retourne:
set[str]: Un ensemble de codes de types de jours (ex: {"TT", "MALADE", ...}).
"""
return {"TT", "MALADE", "CONGE", "RTT", "FERIE"}