Support for Hydro-Québec (Tariff D / Flex D): not based on power, tiered consumption model

Hi @pierre-gilles

I wanted to open a discussion space on GitHub - GladysAssistant/energy-contracts: Gladys Assistant Energy Contracts as JSON · GitHub, but I can only open issues.

Context

I would like to add Hydro-Québec’s residential rates (Québec’s public electricity provider, Canada) to this repo, but the pricing structure does not fit the current model.

How the Current Model Works

Looking at EDF (contracts/edf/base) and other French providers in the repo, the price is always organized around a subscribed power (kVA):

  • a key per power tier (3, 6, 9, 12, 15 kVA…)
  • for each tier: a fixed subscription (annualized to monthly) + a constant variable price per kWh
  • existing variants: base, peak-off-peak (peak/off-peak hours), tempo (blue/white/red day)

Why Hydro-Québec’s Tariff D Does Not Fit This Model

Tariff D (basic residential) has no notion of subscribed power. Its structure is:

  • a fixed daily network access fee, independent of power
  • a progressive price per kWh based on actual daily consumption (not a choice of tier in advance):
    • up to 40 kWh/day: a first price
    • beyond 40 kWh/day: a second, higher price

Rate grid in effect since April 1, 2026 (official source: grille-tarifaire.pdf):

Component Value
Network Access Fee 0.46154 $ CAD / day
Energy, ≤ 40 kWh/day 0.07065 $ CAD / kWh
Energy, > 40 kWh/day 0.11142 $ CAD / kWh

There is also a dynamic pricing option, Flex D, with a reduced off-peak price in winter and a surcharged price during critical peak events announced 24 hours in advance (figures to be precisely verified on the official grid before implementation):

Component Value (to confirm)
Network Access Fee same as Tariff D
Off-peak, winter period (Dec 1–March 31) ≈ 0.04886 $ CAD / kWh
Critical Peak (occasional events) ≈ 0.50 $ CAD / kWh
Rest of the year Standard Tariff D

Other structural differences:

  • currency CAD, not yet used in the repo (only eur seems to be used in practice)
  • no notion of subscribed power to be chosen by the customer

What I Propose to Discuss

  1. How to encode a tariff without subscribed power? A neutral key ("none" / "standard") instead of the kVA tier, or an evolution of the schema to make this dimension optional?
  2. How to encode a tiered kWh rate (price that changes based on the volume consumed in the period, not based on the time or day)? Nothing in the current repo seems to cover this case — the existing variants (base, peak-off-peak, tempo) are all independent of volume.
  3. Would a new type of folder be relevant, for example, tiered-consumption, in addition to base / peak-off-peak / tempo?
  4. Confirmation that the cad currency is supported/accepted by the process.js pipeline / the Gladys frontend that consumes this JSON.

I would appreciate feedback before investing time (well, tokens :wink:) in the CSV + convert.js. Once the format is validated, I can propose a PR with:

  • contracts/hydro-quebec/base (Tariff D)
  • contracts/hydro-quebec/flex (Flex D)

Hello @lmilcent,

Thanks for this very complete topic, it’s perfect timing! :raising_hands:

I’m currently overhauling the energy tracking in Gladys (I presented it here: API : permettre aux intégrations externes de déclarer leurs propres types de contrat énergie - #3 par pierre-gilles).

Contracts are no longer limited to Base / Off-Peak / Tempo: each contract is described by rules, and this covers exactly the points you raise.

Tariff D: this will be possible directly

  • No subscribed power: this field becomes optional.
  • Consumption tiers: « the first 40 kWh of the day at one price, the rest at another » is managed natively.
  • Daily access fees: a fixed amount per day.
  • Canadian dollar: each contract has its own currency (CAD, USD, EUR…).

Concretely, with the prices from your message, Tariff D is written like this:

{
  "tariff_version": 1,
  "components": [
    {
      "key": "energy",
      "kind": "consumption",
      "rules": [
        { "label": "First 40 kWh of the day", "when": { "tier": { "cumulative": "day", "from_kwh": 0, "to_kwh": 40 } }, "price": 0.07065 }
      ],
      "fallback": { "label": "Beyond 40 kWh", "price": 0.11142 }
    },
    { "key": "access", "kind": "fixed", "amount": 0.46154, "per": "day" }
  ]
}

You’ll be able to paste it into Gladys’ contract editor, and it can also be added to the community catalog so everyone can have it in one click.

Flex D: the calculation is planned, the source of critical peaks is missing

The calculation itself is covered: winter period from December 1st to March 31st, reduced price outside peak hours, high price during critical peak events. But since the events are announced the day before, something needs to retrieve them and transmit them to Gladys.

This will be the role of an external integration, which publishes the peak days in a « calendar » that the contract uses. Everything is planned in Gladys for this, but this integration still needs to be written.

Two questions to make sure it matches your bill

  1. For the 40 kWh threshold, does Hydro-Québec calculate it day by day, or as « 40 kWh × number of days » over the entire billing period? This slightly changes the result on days where you exceed 40 kWh.
  2. For Flex D, can you confirm the time slots of the peak events, and if the 40 kWh tiers also apply during the winter?

It’s not out yet, it’s being reviewed before a next version.

Cool to see that you’re revisiting the topic! :tada:

40 kWh Threshold (Tariff D)

Hydro-Québec calculates this cumulatively over the entire billing period, not day by day: the first tier = 40 kWh × number of days in the period, applied to the total consumption of the period.

Concrete example from my bill: 368 kWh over 63 days, threshold = 40 × 63 = 2,520 kWh → everything is billed at the first price (368 × 0.07065 $ = 26.00 $), regardless of whether an individual day exceeded 40 kWh.

In your JSON example, "cumulative": "day" probably doesn’t correspond to the actual method; it would rather be a cumulative total over the entire billing period ("cumulative": "billing_period" or equivalent), without daily resets.

Flex D Based on the Official 2026 Grid (Article 2.72)

Component Price
Network Access Fee, per day 46.154 ¢
Winter, Off-Peak — up to 40 kWh/day 4.886 ¢/kWh
Winter, Off-Peak — remaining energy 9.103 ¢/kWh
Winter, During a Peak Event 46.463 ¢/kWh (single price)
Summer (rest of the year) — up to 40 kWh/day 7.065 ¢/kWh
Summer (rest of the year) — remaining energy 11.142 ¢/kWh

The 40 kWh tier applies during the winter Flex D, outside of an event: it’s only during a peak event that a single price replaces the tiers.

Peak events: between 6 a.m.-10 a.m. and 4 p.m.-8 p.m., every day of the week (including weekends), maximum 120 hours per winter, winter period from December 1 to March 31 inclusive. Rest of the year: Standard Tariff D.

Hydro Quebec Integration

For the external integration that would publish peak days in a calendar, my current integration based on the hydroqc project could support it (I need to adapt the Gladys module). But this project requires account credentials.

Otherwise, I found that Hydro-Québec publishes an official open data portal (https://donnees.hydroquebec.com) with a dataset evenements-pointe that lists all peak events (Winter Credit AND Flex D, filterable by offer: CPC-D, TPC-DPC, etc.), via a standard REST API (OpenDataSoft), without account or authentication. The project Beat-YT/hydropeak-ha (MIT) already relies on it for Home Assistant, without ever touching the customer’s credentials.

This would allow a much simpler and safer external integration: a single public feed shared for all users, instead of having to store each user’s Hydro-Québec credentials. Of course, this doesn’t change the fact that credentials are still necessary to retrieve consumption.

Thanks @lmilcent, that’s exactly what I needed! :folded_hands:

Correction on the threshold: my first JSON applied 40 kWh per day, which is wrong. Since Gladys splits by month, I apply the threshold « 40 kWh × days in the month » to the monthly total (1,240 / 1,200 / 1,120 kWh). On an example, this gives exactly the Hydro-Québec calculation, whereas the « per day » version overestimated by 14%.

Only limitation: your billing period lasts about 2 months. If a high-consumption month is followed by a low-consumption month in the same period, Gladys will count a bit too many kWh at the 2nd level. Leap year February (2028) is also offset by 40 kWh.

Tariff D

{
  "tariff_version": 1,
  "components": [
    { "key": "energy", "kind": "consumption",
      "rules": [
        { "label": "Tier 1", "when": { "months": [1,3,5,7,8,10,12], "tier": { "cumulative": "month", "from_kwh": 0, "to_kwh": 1240 } }, "price": 0.07065 },
        { "label": "Tier 1", "when": { "months": [4,6,9,11], "tier": { "cumulative": "month", "from_kwh": 0, "to_kwh": 1200 } }, "price": 0.07065 },
        { "label": "Tier 1", "when": { "months": [2], "tier": { "cumulative": "month", "from_kwh": 0, "to_kwh": 1120 } }, "price": 0.07065 }
      ],
      "fallback": { "label": "Tier 2", "price": 0.11142 } },
    { "key": "access", "kind": "fixed", "amount": 0.46154, "per": "day" }
  ]
}

Flex D

{
  "tariff_version": 1,
  "calendars": ["hq-critical-peaks"],
  "components": [
    { "key": "energy", "kind": "consumption",
      "rules": [
        { "label": "Peak", "when": { "months": [12,1,2,3], "calendar": { "hq-critical-peaks": "critical-peak" } }, "price": 0.46463 },
        { "label": "Winter Tier 1", "when": { "months": [12,1,3], "tier": { "cumulative": "month", "from_kwh": 0, "to_kwh": 1240 } }, "price": 0.04886 },
        { "label": "Winter Tier 1", "when": { "months": [2], "tier": { "cumulative": "month", "from_kwh": 0, "to_kwh": 1120 } }, "price": 0.04886 },
        { "label": "Winter Tier 2", "when": { "months": [12,1,2,3] }, "price": 0.09103 },
        { "label": "Summer Tier 1", "when": { "months": [5,7,8,10], "tier": { "cumulative": "month", "from_kwh": 0, "to_kwh": 1240 } }, "price": 0.07065 },
        { "label": "Summer Tier 1", "when": { "months": [4,6,9,11], "tier": { "cumulative": "month", "from_kwh": 0, "to_kwh": 1200 } }, "price": 0.07065 }
      ],
      "fallback": { "label": "Summer Tier 2", "price": 0.11142 } },
    { "key": "access", "kind": "fixed", "amount": 0.46154, "per": "day" }
  ]
}

For peaks, the « evenements-pointe » dataset from open data is perfect: a small external integration can read it and publish the peak slots in the hq-critical-peaks calendar, without any identifier. Since the slots come from the data, the rule doesn’t need the 6 a.m.–10 a.m. / 4 p.m.–8 p.m. ranges. hydropeak-ha is a good base for retrieval. If someone wants to take it on, I’m here to help!

Last question: do the kWh consumed during a peak count towards the 40 kWh × days threshold? For now, I count them.

Now that’s a good question! I had no idea, but Claude really helped me with all the documents I gave him :grinning_face_with_smiling_eyes:

For peak kWh: the official tariff grid (article 2.72) describes three distinct and mutually exclusive categories:

  • « Up to 40 kWh of energy per day outside peak events »
  • « Remaining energy consumed outside peak events »
  • « Energy consumed during peak events »

The 40 kWh × days threshold is explicitly scoped to off-peak in the text. Nothing indicates that peak energy feeds this same total — these are two separate meters. Therefore, kWh consumed during a peak event should not count towards the off-peak tier threshold.

This also aligns with the program’s logic: otherwise, a customer who cannot reduce their consumption during a peak event would be penalized twice (peak price + loss of advantageous tier for the rest of the month), which goes against the goal of a dynamic tariff.

For implementation, this would mean: in the rule « Winter Tier 1 / Tier 2 », the window tier.cumulative: "month" should only count kWh outside calendar: critical-peak, not the total monthly gross.

Hi Louis,

Thanks for article 2.72, you were completely right! I’ve updated the Gladys engine with two additions to the tiers:

  • to_kwh_per_day : the threshold becomes « 40 kWh × number of days in the period ». It is calculated on the total, and leap year February is handled.
  • counts_when : you choose which kWh feed the tier. kWh consumed during a peak no longer eat your off-peak tier.

I’ve checked a few cases, to the centime near the grid:

  • January with irregular days (1 220 kWh) : $86.19 ;
  • Leap year February 2028 : $98.11 ;
  • Flex D in January with 20 peak hours : $116.90 (without the correction, we were at $118.45).

Tariff D

{
  "tariff_version": 1,
  "components": [
    { "key": "energy", "kind": "consumption",
      "rules": [
        { "label": "Tier 1", "when": { "tier": { "cumulative": "billing_period", "from_kwh_per_day": 0, "to_kwh_per_day": 40 } }, "price": 0.07065 }
      ],
      "fallback": { "label": "Tier 2", "price": 0.11142 } },
    { "key": "access", "kind": "fixed", "amount": 0.46154, "per": "day" }
  ]
}

Flex D

{
  "tariff_version": 1,
  "calendars": ["hq-critical-peaks"],
  "components": [
    { "key": "energy", "kind": "consumption",
      "rules": [
        { "label": "Peak", "when": { "calendar": { "hq-critical-peaks": "critical-peak" } }, "price": 0.46463 },
        { "label": "Winter Tier 1", "when": { "season": { "from": "12-01", "to": "03-31" }, "tier": { "cumulative": "billing_period", "from_kwh_per_day": 0, "to_kwh_per_day": 40, "counts_when": { "not_calendar": { "hq-critical-peaks": "critical-peak" } } } }, "price": 0.04886 },
        { "label": "Winter Tier 2", "when": { "season": { "from": "12-01", "to": "03-31" } }, "price": 0.09103 },
        { "label": "Summer Tier 1", "when": { "tier": { "cumulative": "billing_period", "from_kwh_per_day": 0, "to_kwh_per_day": 40 } }, "price": 0.07065 }
      ],
      "fallback": { "label": "Summer Tier 2", "price": 0.11142 } },
    { "key": "access", "kind": "fixed", "amount": 0.46154, "per": "day" }
  ]
}

Remaining limitation: Gladys splits billing by month, while your Hydro-Québec periods are about 2 months. The gap only plays if a heavy month and a light month end up in the same period.

For peaks to be actually billed, there remains the integration that reads events on Hydro-Québec’s open data.

Excellent! Everything seems correct to me, too bad for the small flaw, it’s unlikely (unless you leave your home) to have two very different months on consumption during the winter, given the insulation of buildings and the type of heating here :rofl:

When it’s released, I’ll update my module to synchronize the Hydro-Québec peak scheduling.

Hi @pierre-gilles

Thanks for the contract overhaul, I’ve read #3130 and the SDK sync. As planned, I’ll update the Hydro-Québec module to populate the hq-critical-peaks calendar: the Flex D peaks (TPC-DPC) read from open data, published in 30-minute slots, with the Tarif D and Flex D models we’ve validated together. All Flex D events published since December 2024 are on the dot (6 a.m.–9 a.m., 6 a.m.–10 a.m., 4 p.m.–8 p.m.), so alignment isn’t an issue.

However, I’m still stuck on getting the cost actually calculated:

  1. Hydro-Québec only provides consumption with about a day’s delay (hourly data from the previous day). Yet the only path planned for integration, the cumulative index, is processed in real time: the entire index gap goes into the current half-hour, and a state older than the cursor is ignored.
  2. Daily consumption isn’t enough for Flex D: a day is billed at its start (midnight), so it never falls into a peak slot. And an integration can’t link a consumption/cost pair (energy_parent_id) on its own.

Would you consider:

  • letting an integration publish a dated thirty-minutes-consumption (created_at in the past), from which the core would derive the cost functionality as it does for an index, then bill it upon arrival?
  • a dated requestEnergyRecalculation({ from })? The current retry only covers 48 hours, and only contracts that read a calendar from the integration, so not Tarif D alone.

On my end, I’d split each hour into two equal half-hours. Since peaks start on the hour, nothing would be misattributed.