API: permitir que las integraciones externas declaren sus propios tipos de contrato de energía

Hola a todos,

Tras la discusión sobre las tarifas de fin de semana y la respuesta de @pierre-gilles en Gladys#2999, aquí está la solicitud de funcionalidad correspondiente.

El principio, primero. Pierre-Gilles propuso mantener el núcleo genérico y permitir que las integraciones externas declaren nuevos tipos de contrato, en lugar de apilar en el núcleo casos específicos de cada país. Estoy completamente de acuerdo, y al mirar el código, encuentro que es aún más pertinente de lo que pensaba: la arquitectura ya va en esa dirección, principalmente falta abrir la puerta.

Hay que señalar que @mutmut ya ha abierto una solicitud sobre la gestión de tarifas según el día, la temporada y las vacaciones. Esta no la reemplaza: la suya describe la necesidad del usuario, esta describe el mecanismo técnico que permitiría responder a ella sin sobrecargar el núcleo. Las dos son complementarias.

Lo que ya existe y que va en la dirección correcta

server/services/energy-monitoring/contracts/contracts.calculateCost.js ya es un diccionario indexado por tipo de contrato, donde cada entrada es una función de cálculo con una firma uniforme:

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

Es exactamente un punto de extensión. Mejor aún: el parámetro context ya se utiliza para inyectar datos externos — Tempo recibe edfTempoHistoricalMap, es decir, un calendario de colores de días recuperado en otro lugar. El precedente de un tipo de contrato que depende de una fuente de datos externa ya está establecido.

En cuanto al almacenamiento, hour_slots es una simple cadena libre y subscribed_power también. Una integración externa podría codificar en ellas lo que desee sin tocar el esquema.

Lo que bloquea

Cuatro obstáculos, del más difícil al más simple:

  1. El modelo SQL. En server/models/energy_price.js, la columna contract es un DataTypes.ENUM(...ENERGY_CONTRACT_TYPES_LIST), y day_type un ENUM(...ENERGY_PRICE_DAY_TYPES_LIST). Este es el verdadero bloqueo: añadir un tipo requiere una migración de base de datos, algo que una integración externa no puede hacer.
  2. La lista fija ENERGY_CONTRACT_TYPES en server/utils/constants.js, que alimenta este ENUM y la validación del servidor.
  3. El diccionario de cálculo no es registrable: los tres controladores están escritos en el módulo.
  4. El front-end, con KNOWN_CONTRACT_TYPES en ImportPrices.jsx y los rótulos en contractTypes de los archivos i18n.

Lo que propongo

a) Cambiar contract y day_type de ENUM a STRING, desplazando la validación a la capa de aplicación (contra la lista de tipos registrados, núcleo + integraciones). Una convención de nombres por integración evitaría colisiones, por ejemplo <service>:<type>. Este es el cambio estructurante: sin él, ninguna extensión externa es posible.

b) Exponer una API de registro en el servicio, en el espíritu de:

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

La firma ya es la de los controladores existentes, por lo que los tres tipos actuales podrían migrarse a este mecanismo sin cambiar nada de su lógica — buena manera de validar la API de paso.

c) Hacer que la pantalla de importación sea controlable por los datos: la lista de tipos y el rótulo provienen de los tipos registrados, con un retroceso limpio cuando no existe ninguna traducción. El selector de intervalos horarios actual seguiría siendo el comportamiento predeterminado.

d) Opcional, para más tarde: permitir que la integración declare el tipo de editor de precios que necesita, o incluso que proporcione su propia pantalla.

Con esto, una integración « energía Francia » podría llevar Zen Week-End, Tempo, las ofertas de Engie / OHM / Enercoop y los casos estacionales de @mutmut, sin que el núcleo tenga que conocer el calendario de días festivos franceses.

Una pregunta abierta

Hoy en día, las tarifas viven en el repositorio comunitario energy-contracts, y es valioso: cuando alguien actualiza los precios de EDF, todos se benefician. Si los tipos específicos de un país se van a una integración externa, ¿adónde van los datos de precios? ¿Se quedan en energy-contracts con la integración que solo añade la lógica de cálculo, o también se mudan?

Tengo una preferencia por la primera opción — mantener un único repositorio de tarifas, compartido, y solo sacar el código de negocio — pero es realmente una pregunta, no una posición firme.

¡Gracias!

Un pequeño resumen sobre esta solicitud, que quedó sin respuesta: en lugar de esperar, intenté algo con Fable 5.1: ¡crear la PR! Intentando ajustarme al máximo a lo que ya existe en el núcleo.

PR: https://github.com/GladysAssistant/Gladys/pull/3099

Aquí hay algunas aclaraciones de la IA:

Al releer el código, cambié de opinión con respecto a mi primer mensaje. Hacer que la función de cálculo sea responsabilidad de la integración, como yo proponía, habría obligado al núcleo a consultar la integración para cada muestra de 30 minutos: decenas de miles de comandos WebSocket confirmados para un recálculo desde el principio, y un código de terceros no auditado que decide los montos facturados. No es razonable.

La propuesta en la PR es más modesta y sigue exactamente el esquema del tipo weather (spec B.18):

  • Un nuevo tipo de integración externa energy-calendar. El núcleo le pide, una vez por cálculo y en un rango de fechas, el tipo de día de cada día (weekday, weekend, holiday… el vocabulario es libre, es la integración la que lo publica). La respuesta está normalizada y limitada antes de entrar en el núcleo, como el clima. Sin pantallas de dispositivos, sin dispositivos: una API dedicada.
  • Un tipo de contrato genérico day-type en el núcleo. Sus precios llevan un day_type libre y hour_slots opcionales. El cálculo es el de Tempo, con el tipo de día del calendario en lugar del color: se filtran los precios por tipo de día, luego por franja; un precio sin franja vale para todo el día (es la línea « fin de semana » de un contrato EDF Zen Week-End). El núcleo no conoce ningún calendario nacional.
  • day_type pasa de un ENUM de los colores Tempo a una cadena validada. En SQLite, un ENUM es una columna TEXT, por lo que no hay migración, y Tempo sigue funcionando como tal.
  • Del lado de la interfaz: el contrato en el selector del editor de precios, el sufijo -day-type reconocido por la pantalla de importación, y el tipo en las pantallas de integración externa. Traducciones en, fr, de.
  • El documento docs/specs/external-integrations.md tiene una sección B.19 que detalla todo esto.

Lo que seguiría, fuera de este repositorio: un manejador onEnergyCalendarGetDayTypes en el SDK, la adición del tipo en el esquema de manifiesto de integration-store, una integración « calendario Francia » (fin de semana y días festivos, calculados localmente), y la tarifa EDF Zen Week-End en energy-contracts. Las tarifas seguirían centralizadas, la integración solo proporcionaría el calendario.

Pruebas de servidor en verde con 100% de cobertura en las líneas añadidas, lint, traducciones y compilación frontal también. Obviamente, estoy abierto a cualquier comentario sobre el diseño o el nombramiento antes de seguir adelante.

¡Hola a todos!

Hoy, el seguimiento de energía en Gladys tiene tres tipos de contratos: Base, Horas Válidas y Tempo. Tan pronto como un contrato se sale de estos casos (oferta de fin de semana, días festivos, tarifa estacional, precio spot…), es imposible usarlo.

Estoy cambiando esto a fondo :raising_hands:

Lo que viene:

  • Un verdadero « contrato » por contador, creado en unos pocos clics: elección del contador, elección del modelo, ajuste de los parámetros (sus horas válidas, su suscripción…), y una vista previa del costo en sus últimos 7 días antes de guardar.
  • Muchos más tarifas posibles: horas válidas, fines de semana, días festivos, vacaciones escolares, estaciones, niveles de consumo, precios que cambian todos los días… y no solo en Francia.
  • Calendarios tarifarios: días festivos, colores Tempo, vacaciones escolares… Gladys los usa para aplicar el precio correcto en el momento adecuado.
  • Un widget « precio actual » en el tablero de control, que muestra el precio del momento y el próximo cambio.
  • Escenas basadas en el precio: por ejemplo, « cuando pasamos a horas válidas, iniciar el calentador de agua ».
  • Contratos publicables por la comunidad, sin esperar una actualización de Gladys: ya sea simplemente añadidos al catálogo comunitario, o en forma de integración externa para los casos más complejos (calendarios que mantener actualizados, cálculo de precio específico…).

Para las capturas de abajo, he creado una pequeña integración de prueba, « Francia Fin de Semana & Vacaciones », con tres contratos: horas válidas + fin de semana + días festivos, una oferta reducida durante las vacaciones escolares, y una oferta de fidelidad calculada por la propia integración.

Sus contratos actuales serán migrados automáticamente, no tendrá que hacer nada.

Aún está en revisión antes de llegar a una próxima versión :slight_smile:

La PR: https://github.com/GladysAssistant/Gladys/pull/3130

La imagen Docker si quieres probar:

ghcr.io/gladysassistant/gladys-preview:claude-amazing-einstein-6f4ub7

¡Me encanta :heart_eyes: !!! No creo que tenga tiempo de probar tu imagen pronto, pero esto responde a mi problema (y a muchos otros de paso :stuck_out_tongue: )

impecable, podré eliminar un dispositivo virtual con 6 funcionalidades, un flujo de node-red y una escena de actualización :partying_face:

¡Me interesaría tener probadores en copias de bases de producción para verificar que los cálculos sean los mismos antes y después!

¿Esto permitirá actualizaciones dinámicas de precios? Por ejemplo, en Finlandia no es inusual tener un contrato de electricidad donde el precio sigue el precio spot de la electricidad y, por lo tanto, el precio cambia cada 15 minutos y los precios son diferentes cada día. Puedo obtener fácilmente los precios de mañana de las APIs públicas, pero aún no he encontrado una buena manera de integrarlo en Gladys y, en su lugar, he implementado toda la lógica personalizada directamente en Node-RED para activar interruptores y aprovechar las horas más baratas del día.

He podido probar por mi parte.

Aquí está el estado anterior :


Luego recalculé mediante estos 2 botones :

Y aquí está el resultado :


Por lo tanto, para mí todo está bien :wink:

@sebasth ¡Gracias por tu respuesta! He integrado el caso finlandés para que sea posible, gracias por haberlo reportado :slight_smile:

@prohand ¡Gracias por haberlo probado! Genial si funciona como antes :ok_hand: