API: allow external integrations to declare their own energy contract types

Hello everyone,

Following the discussion on weekend rates and @pierre-gilles’ response on Gladys#2999, here is the corresponding feature request.

The principle, first. Pierre-Gilles proposed keeping the core generic and allowing external integrations to declare new types of contracts, rather than stacking country-specific cases in the core. I completely agree, and looking at the code I find that it’s even more relevant than I thought: the architecture already goes in this direction, it mainly lacks opening the door.

Note that @mutmut has already opened a request on managing rates according to the day, season, and holidays. This one does not replace it: his describes the user need, this one describes the technical mechanism that would allow responding to it without overloading the core. Both are complementary.

What already exists and is going in the right direction

server/services/energy-monitoring/contracts/contracts.calculateCost.js is already a dictionary indexed by contract type, where each entry is a calculation function with a uniform signature:

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

This is exactly an extension point. Better: the context parameter is already used to inject external data — Tempo receives edfTempoHistoricalMap, i.e., a day color calendar retrieved elsewhere. The precedent of a contract type that depends on an external data source is therefore already established.

On the storage side, hour_slots is a simple free string and subscribed_power too. An external integration could encode whatever it wants there without touching the schema.

What’s blocking

Four locks, from the hardest to the simplest:

  1. The SQL model. In server/models/energy_price.js, the contract column is a DataTypes.ENUM(...ENERGY_CONTRACT_TYPES_LIST), and day_type an ENUM(...ENERGY_PRICE_DAY_TYPES_LIST). This is the real blocker: adding a type requires a database migration, which an external integration cannot do.
  2. The hard-coded list ENERGY_CONTRACT_TYPES in server/utils/constants.js, which feeds this ENUM and server validation.
  3. The calculation dictionary is not registerable: the three handlers are hard-coded in the module.
  4. The front end, with KNOWN_CONTRACT_TYPES hard-coded in ImportPrices.jsx and the labels in contractTypes of the i18n files.

What I propose

a) Change contract and day_type from ENUM to STRING, moving validation to the application layer (against the list of registered types, core + integrations). A naming convention per integration would avoid collisions, for example <service>:<type>. This is the structuring change: without it, no external extension is possible.

b) Expose a registration API on the service, in the spirit of:

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

The signature being already that of the existing handlers, the three current types could be migrated to this mechanism without changing anything to their logic — a good way to validate the API in passing.

c) Make the import screen data-driven: the list of types and the label come from the registered types, with a clean fallback when no translation exists. The current time slot selector would remain the default behavior.

d) Optional, for later: allow the integration to declare the type of price editor it needs, or even to provide its own screen.

With this, a “France energy” integration could handle Zen Week-End, Tempo, Engie / OHM / Enercoop offers, and @mutmut’s seasonal cases, without the core having to know the calendar of French holidays.

An open question

Today, the rate grids live in the community repository energy-contracts, and that’s valuable: when someone updates the EDF prices, everyone benefits. If country-specific types go into an external integration, where do the price data go? Do they stay in energy-contracts with the integration just adding the calculation logic, or do they move too?

I have a preference for the first option — keep a single repository of grids, shared, and only take out the business code — but it’s really a question, not a fixed position.

Thanks!