API : permettre aux intégrations externes de déclarer leurs propres types de contrat énergie

Bonjour à tous,

Suite à la discussion sur les tarifs week-end et à la réponse de @pierre-gilles sur Gladys#2999, voici la demande de fonctionnalité correspondante.

Le principe, d’abord. Pierre-Gilles proposait de garder le cœur générique et de permettre aux intégrations externes de déclarer de nouveaux types de contrat, plutôt que d’empiler dans le cœur des cas propres à chaque pays. Je suis complètement d’accord, et en regardant le code je trouve que c’est encore plus pertinent que je ne le pensais : l’architecture va déjà dans ce sens, il manque surtout d’ouvrir la porte.

À noter que @mutmut a déjà ouvert une demande sur la gestion des tarifs selon le jour, la saison et les vacances. Celle-ci ne la remplace pas : la sienne décrit le besoin utilisateur, celle-ci décrit le mécanisme technique qui permettrait d’y répondre sans alourdir le cœur. Les deux sont complémentaires.

Ce qui existe déjà et qui va dans le bon sens

server/services/energy-monitoring/contracts/contracts.calculateCost.js est déjà un dictionnaire indexé par type de contrat, où chaque entrée est une fonction de calcul avec une signature uniforme :

(energyPricesAtConsumptionDate, consumptionDate, consumptionValue, systemTimezone, context) => cost

C’est exactement un point d’extension. Mieux : le paramètre context sert déjà à injecter des données externes — Tempo y reçoit edfTempoHistoricalMap, c’est-à-dire un calendrier de couleurs de jours récupéré ailleurs. Le précédent d’un type de contrat qui dépend d’une source de données extérieure est donc déjà établi.

Côté stockage, hour_slots est une simple chaîne libre et subscribed_power aussi. Une intégration externe pourrait y encoder ce qu’elle veut sans toucher au schéma.

Ce qui bloque

Quatre verrous, du plus dur au plus simple :

  1. Le modèle SQL. Dans server/models/energy_price.js, la colonne contract est un DataTypes.ENUM(...ENERGY_CONTRACT_TYPES_LIST), et day_type un ENUM(...ENERGY_PRICE_DAY_TYPES_LIST). C’est le vrai blocage : ajouter un type demande une migration de base, ce qu’une intégration externe ne peut pas faire.
  2. La liste en dur ENERGY_CONTRACT_TYPES dans server/utils/constants.js, qui alimente cet ENUM et la validation serveur.
  3. Le dictionnaire de calcul n’est pas enregistrable : les trois handlers sont écrits en dur dans le module.
  4. Le front, avec KNOWN_CONTRACT_TYPES en dur dans ImportPrices.jsx et les libellés dans contractTypes des fichiers i18n.

Ce que je propose

a) Passer contract et day_type de ENUM à STRING, en déplaçant la validation dans la couche applicative (contre la liste des types enregistrés, cœur + intégrations). Une convention de nommage par intégration éviterait les collisions, par exemple <service>:<type>. C’est le changement structurant : sans lui, aucune extension externe n’est possible.

b) Exposer une API d’enregistrement sur le service, dans l’esprit de :

gladys.energyMonitoring.registerContractType({
  id: 'edf-zen-week-end',
  calculateCost: async (prices, date, value, timezone, context) => { ... },
});

La signature étant déjà celle des handlers existants, les trois types actuels pourraient être migrés vers ce mécanisme sans rien changer à leur logique — bonne façon de valider l’API au passage.

c) Rendre l’écran d’import pilotable par les données : la liste des types et le libellé viennent des types enregistrés, avec un repli propre quand aucune traduction n’existe. Le sélecteur de créneaux horaires actuel resterait le comportement par défaut.

d) Optionnel, pour plus tard : permettre à l’intégration de déclarer le type d’éditeur de prix dont elle a besoin, voire de fournir son propre écran.

Avec ça, une intégration « énergie France » pourrait porter Zen Week-End, Tempo, les offres Engie / OHM / Enercoop et les cas saisonniers de @mutmut, sans que le cœur ait à connaître le calendrier des jours fériés français.

Une question ouverte

Aujourd’hui, les grilles tarifaires vivent dans le dépôt communautaire energy-contracts, et c’est précieux : quand quelqu’un met à jour les prix EDF, tout le monde en profite. Si les types spécifiques à un pays partent dans une intégration externe, où vont les données de prix ? Elles restent dans energy-contracts avec l’intégration qui vient juste ajouter la logique de calcul, ou elles déménagent aussi ?

J’ai une préférence pour la première option — garder un dépôt unique de grilles, mutualisé, et ne sortir que le code métier — mais c’est vraiment une question, pas une position arrêtée.

Merci !

Petit retour sur cette demande, restée sans réponse : plutôt que d’attendre, j’ai tenté un truc avec Fable 5.1 : creer la PR !!! En essayant de coller au maximum à ce qui existe déjà dans le cœur.

PR : feat(energy): energy-calendar integration type and day-type contract by guim31 · Pull Request #3099 · GladysAssistant/Gladys · GitHub

Voici quelques précisions de l’IA :

En relisant le code, j’ai changé d’avis par rapport à mon premier message. Faire porter la fonction de calcul par l’intégration, comme je le proposais, aurait obligé le cœur à interroger l’intégration pour chaque échantillon de 30 minutes : des dizaines de milliers de commandes WebSocket acquittées pour un recalcul depuis le début, et un code tiers non audité qui décide des montants facturés. Ce n’est pas raisonnable.

La proposition dans la PR est plus modeste et reprend exactement le schéma du type weather (spec B.18) :

  • Un nouveau type d’intégration externe energy-calendar. Le cœur lui demande, une fois par calcul et sur une plage de dates, le type de jour de chaque journée (weekday, weekend, holiday… le vocabulaire est libre, c’est l’intégration qui le publie). La réponse est normalisée et bornée avant d’entrer dans le cœur, comme la météo. Pas d’écran appareils, pas de device : une API dédiée.
  • Un type de contrat générique day-type dans le cœur. Ses prix portent un day_type libre et des hour_slots optionnels. Le calcul est celui de Tempo, avec le type de jour du calendrier à la place de la couleur : on filtre les prix par type de jour, puis par créneau ; un prix sans créneau vaut pour toute la journée (c’est la ligne « week-end » d’un contrat EDF Zen Week-End). Le cœur ne connaît aucun calendrier national.
  • day_type passe d’un ENUM des couleurs Tempo à une chaîne validée. Sous SQLite un ENUM est une colonne TEXT, donc pas de migration, et Tempo continue de fonctionner tel quel.
  • Côté interface : le contrat dans le sélecteur de l’éditeur de prix, le suffixe -day-type reconnu par l’écran d’import, et le type dans les écrans d’intégration externe. Traductions en, fr, de.
  • La spec docs/specs/external-integrations.md a une section B.19 qui détaille tout ça.

Ce qui suivrait, hors de ce dépôt : un handler onEnergyCalendarGetDayTypes dans le SDK, l’ajout du type dans le schéma de manifeste de integration-store, une intégration « calendrier France » (week-ends et jours fériés, calculés localement), et la grille EDF Zen Week-End dans energy-contracts. Les grilles tarifaires resteraient donc centralisées, l’intégration ne fournissant que le calendrier.

Tests serveur au vert avec 100 % de couverture sur les lignes ajoutées, lint, traductions et build front aussi. Je suis évidemment preneur de tout retour sur le découpage ou le nommage avant d’aller plus loin.