Skip to content

How Energy Allocation and Billing Works

This guide explains the exact billing logic used by OpenZEV so you can understand and verify participant invoices.

Key Principle: Fair Sharing

OpenZEV allocates energy fairly at the timestamp level (each 15-minute or hourly reading), not just monthly totals.

Concept: At each moment, community production is shared among participants proportional to their consumption at that moment. Remaining consumption comes from the grid.

Inputs for One Invoice

For a participant and billing period, OpenZEV loads:

  • Participant readings:
    • Consumption (IN) from participant metering points
    • Feed-in/production (OUT) from participant metering points
  • ZEV totals per timestamp:
    • Total community consumption (all participants)
    • Total community production (all participants)
  • Active tariffs for the participant's ZEV

Timestamp-Level Allocation

At each moment \(t\) (e.g., one hour), OpenZEV calculates:

1) Local Pool

The amount of energy that can be matched locally (within community):

\[L(t) = \min(P_z(t), C_z(t))\]

Where:

  • \(P_z(t)\) = total ZEV production at timestamp \(t\)
  • \(C_z(t)\) = total ZEV consumption at timestamp \(t\)
  • \(L(t)\) = local pool

Intuition: Local pool is the overlapping overlap between "how much is being produced" and "how much is being consumed" at that exact moment. Can never exceed either.

Example: Local Pool Calculation

Scenario a) Production < Consumption

  • ZEV produces 15 kW, community consumes 25 kW
  • Local pool = min(15, 25) = 15 kW (limited by production)
  • Remaining 10 kW demand goes to grid

Scenario b) Production > Consumption

  • ZEV produces 30 kW, community consumes 20 kW
  • Local pool = min(30, 20) = 20 kW (limited by demand)
  • Remaining 10 kW production is exported to grid (not billed to participants)

2) Participant's Local Energy Share

Each participant gets a fair share of the local pool proportional to their consumption:

\[E_{local,p}(t) = \begin{cases} \min\left(C_p(t),\; L(t) \cdot \frac{C_p(t)}{C_z(t)}\right) & \text{if } C_z(t) > 0 \\ 0 & \text{if } C_z(t) = 0 \end{cases}\]

Where:

  • \(C_p(t)\) = participant consumption at time \(t\)
  • \(C_z(t)\) = total ZEV consumption at time \(t\)
  • \(E_{local,p}(t)\) = participant's local energy

Intuition: If a participant consumes 20% of total community demand at that moment, they get 20% of the local pool (but never more than their own consumption).

3) Participant's Grid Energy

The remainder of consumption must come from external grid:

\[E_{grid,p}(t) = C_p(t) - E_{local,p}(t)\]

Physical vs. Billing Note: This is an allocation model, not a physical electron trace. A participant can be allocated grid energy even in their moment of overproduction—that production goes to the community pool, and they consume allocated local/grid energy at other times.

Worked Example

Timestamp: 2026-01-15, 14:00 (solar peak)

Community state:

  • Total consumption: 25 kW (all participants)
  • Total production: 15 kW (all solar panels)
  • Local pool: min(15, 25) = 15 kW

Participant Alice:

  • Consumption: 10 kW (40% of community)
  • As % of community demand: 10/25 = 40%
  • Alice's local share: 15 kW × 40% = 6.0 kW
  • Alice's grid share: 10 kW − 6.0 kW = 4.0 kW

Participant Bob:

  • Consumption: 15 kW (60% of community)
  • As % of community demand: 15/25 = 60%
  • Bob's local share: 15 kW × 60% = 9.0 kW
  • Bob's grid share: 15 kW − 9.0 kW = 6.0 kW

Check: 6.0 + 9.0 = 15.0 ✓ (all local pool allocated)

Period Aggregation

Over a full billing period (e.g., calendar month), readings are summed:

  • Total period local energy: Sum of \(E_{local,p}(t)\) for all timestamps
  • Total period grid energy: Sum of \(E_{grid,p}(t)\) for all timestamps
  • Total period feed-in: Sum of any participant production (OUT readings)

Example: Over January (744 hours)

  • Alice accumulated: 168 kWh local + 98 kWh grid = 266 kWh total
  • Bob accumulated: 240 kWh local + 147 kWh grid = 387 kWh total

Tariff Application

Once energy is allocated, tariffs are applied per energy type:

Energy Type Tariff Applied Price
Local energy Local tariff (HT/NT by hour) e.g., 0.10–0.12 CHF/kWh
Grid energy Grid tariff (HT/NT by hour) e.g., 0.18–0.28 CHF/kWh
Feed-in production Feed-in tariff e.g., 0.08 CHF/kWh

Continuing the example:

Alice's charges (Jan, sample rates):

  • Local energy: 168 kWh × 0.11 CHF/kWh (average HT/NT) = CHF 18.48
  • Grid energy: 98 kWh × 0.23 CHF/kWh (average HT/NT) = CHF 22.54
  • Feed-in credit (if any): 5 kWh × 0.08 CHF/kWh = CHF −0.40
  • Fixed fee: CHF 50/month = CHF 50.00

Invoice subtotal: 18.48 + 22.54 − 0.40 + 50.00 = CHF 90.62

If the ZEV is VAT-registered (8.1%):

  • VAT: 90.62 × 0.081 = CHF 7.34 (rounded)
  • Invoice total: CHF 97.96

Fixed-Fee Behavior

Non-energy tariffs follow special rules:

  • Monthly fee: Charged once per billing month in the invoice period
  • Yearly fee: Charged as 1/12 per billing month (CHF price ÷ 12)
  • Per-metering-point fee: Charged for each active meter per month
  • Shared fee: One community-wide amount divided between the participants

Exact invoice engine keys used for fixed-fee tariffs:

  • monthly_fee: charged per intersecting month in the invoice period
  • yearly_fee: monthly installment (fixed_price_chf / 12) per intersecting month
  • per_metering_point_monthly_fee: charged per active metering point per intersecting month
  • per_metering_point_yearly_fee: monthly installment (fixed_price_chf / 12) per active metering point per intersecting month
  • shared_monthly_fee: fixed_price_chf divided by the number of participants active in each intersecting month
  • shared_yearly_fee: monthly installment (fixed_price_chf / 12) divided the same way

Negative fixed prices are represented as credit invoice items.

Example: ZEV with 3 participants, each with 1 meter

January, per participant:

  • Monthly admin fee: CHF 50 → CHF 50 each
  • Meter fee (1 meter each): CHF 5 → CHF 5 each
  • Shared caretaker fee, CHF 90 for the community → CHF 30 each

So each January invoice carries CHF 85 of fixed fees, and the ZEV collects 3 × 50 + 3 × 5 + 90 = CHF 255.

Note the difference: a monthly fee of CHF 50 is CHF 50 per participant (CHF 150 collected), while a shared fee of CHF 90 is CHF 90 for the community however many members it has.

Shared Fees and Changing Membership

A shared fee is divided per month, so the split follows membership as it changes. Each participant is charged only for the months they were a member, and each month's amount is collected exactly once.

Example — CHF 60/month shared, billed January to March:

Jan Feb Mar Total
Alice (whole period) 20.00 20.00 20.00 60.00
Bob (whole period) 20.00 20.00 20.00 60.00
Carol (joins Feb 1) — 20.00 20.00 40.00
Dave (leaves Jan 31) 20.00 — — 20.00
Collected 60.00 60.00 60.00 180.00

Dave still receives an invoice for the period — anyone active at any point in it does — and pays for the single month he was a member of. January is divided three ways because Dave was there; February and March are divided three ways because Carol has replaced him.

If the amount does not divide evenly, each invoice rounds to the centime on its own, so the community may end up a centime or two short: CHF 100 across 3 participants bills 33.33 each and collects CHF 99.99. See Rounding and VAT below.

A shared fee can also be divided by allocation weight instead of per head — see Split key. The month-by-month logic above is identical; only the denominator changes.

Common-Area (Community) Metering Points

Everything above attributes each reading to one participant: whoever held the meter at that moment. A common-area meter — stairwell, lift, laundry, shared heat pump — measures energy nobody uses alone.

Marking an assignment as Community changes how those readings are billed:

  1. Price once. The meter's reading is split into local and grid energy against the community pool, and priced with the ordinary tariffs — exactly the same arithmetic as any other meter. Nothing special happens here.
  2. Allocate second. The resulting kWh and francs are then divided between every eligible participant by their allocation weight.

The holder of record gets no special treatment: they pay their weighted share like everybody else.

Example — a 12 kWh common-area draw on one day, four participants:

Participant Weight Share Billed
Alice 1 12.5 % 1.5 kWh
Bob 1 12.5 % 1.5 kWh
Carol 2 25.0 % 3.0 kWh
Dave 4 50.0 % 6.0 kWh
Total 8 100 % 12 kWh

These kWh are added to each participant's own total_local_kwh / total_grid_kwh totals, and their invoice lines carry a Gemeinschaftsanteil (Community share) marker so the shared portion is visible next to their personal consumption.

Eligibility is checked per day

Community energy uses the reading's date, not the month. A participant who joins on 16 February pays nothing towards common-area energy measured on 15 February, and a leaver's share stops on their leave date.

Fees work per month, energy per day. A per-metering-point fee on a community meter is a monthly charge, so anyone who was a member for any part of the month shares it. Community energy is date-accurate. This is the same distinction that already applies to personal fees versus personal energy.

Production is shared symmetrically

If the community meter is a production or bidirectional meter, its output is credited to eligible participants using the same weights and the same eligibility rule as consumption.

A meter can change mid-period

Allocation is resolved per reading, so a meter that is Personal in January and Community from February bills correctly on both sides: the holder is charged in full for January, and February is split. Nothing is lost and nothing is billed twice.

Rounding and VAT

Precision:

  • Energy quantities: 4 decimals (0.0001 kWh), rounded half-up
  • Unit prices: 5 decimals (e.g., 0.12345 CHF/kWh)
  • Line item totals: Rounded to 2 decimals (CHF 0.01)
  • Invoice subtotal: Rounded to 2 decimals

VAT depends on the ZEV's VAT treatment:

  • VAT-registered — VAT is added on top of the subtotal as its own line
  • Not registered — fold VAT into prices — grid energy, grid fees, levies and metering lines are grossed up by the rate; no VAT line
  • Not VAT-registered — no VAT at all
  • The rate is the one active on the invoice period's end date, from Platform → System Settings → VAT
  • If no rate is active, VAT defaults to 0%

Final total: $\(\text{Invoice Total} = \text{Subtotal} + \text{VAT}\)$

Billing Flow Diagram

flowchart TD
  A[Start invoice generation] --> B[Load participant IN/OUT readings in period]
  B --> C[Load ZEV total IN/OUT per timestamp]
  C --> D[Load active tariffs + tariff periods]
  D --> E[For each participant IN reading timestamp]
  E --> F[Compute local pool = min ZEV production, ZEV consumption]
  F --> G[Allocate participant local share proportionally]
  G --> H[Compute participant grid remainder]
  H --> I[Apply local/grid energy tariffs at timestamp]
  I --> J[Apply feed-in tariffs to participant OUT readings]
  J --> K[Apply fixed-fee tariffs by month / metering point rules]
  K --> L[Round item totals + subtotal]
  L --> M[Apply VAT per the ZEV's VAT treatment]
  M --> N[Create invoice + line items]
  N --> O[End]

Implementation reference: backend/invoices/engine.py (generate_invoice).

Participant Trust & Verification

Every participant should be able to verify their invoice:

  1. Period check: Invoice period matches expected billing window ✓
  2. Energy split check: Local + Grid = Total consumption (minus feed-in) ✓
  3. Tariff check: Line item prices match published tariffs ✓
  4. Credit check: Feed-in and other credits appear as negatives ✓
  5. Rounding check: Line totals and VAT are CHF cents ✓
  6. Total check: Subtotal + VAT = Invoice total ✓

If any check fails, the invoice should be reviewed and corrected.

Why Timestamp-Level Allocation?

Simpler approaches (monthly totals, last-hour precedence) can be unfair:

  • Monthly total: Participant might produce heavily in June but consume in January; monthly total loses timing fairness
  • Allocation by timing: Timestamp-level correctly captures the moment-by-moment local/grid split

Edge Cases

Zero Community Consumption

If \(C_z(t) = 0\) (no one is consuming), no one is allocated local energy:

  • \(E_{local,p}(t) = 0\) for all participants
  • Participant feed-in is not charged

Partial Metering

If a participant's meter is missing readings in a period:

  • Nothing is estimated — the missing intervals count as no energy
  • The period's readiness check on Overview flags the gap before you generate
  • Import the missing data, then regenerate the (draft) invoice

Meter Replacement

If a meter was replaced mid-period:

  • Old meter: Valid To = replacement date
  • New meter: Valid From = replacement date
  • Readings from both meters are included
  • No double-billing or gaps

Worked Example: Two-Timestamp Period

Assume two timestamps in the same invoice period:

Timestamp t1 (morning, cloudy)

  • Community consumption: 20 kW
  • Community production: 10 kW
  • Local pool: min(10, 20) = 10 kW
  • Alice consumption: 8 kW → Local: 10 × (8/20) = 4.0 kW, Grid: 4.0 kW
  • Bob consumption: 12 kW → Local: 10 × (12/20) = 6.0 kW, Grid: 6.0 kW

Timestamp t2 (afternoon, sunny)

  • Community consumption: 12 kW
  • Community production: 30 kW
  • Local pool: min(30, 12) = 12 kW
  • Alice consumption: 6 kW → Local: 12 × (6/12) = 6.0 kW, Grid: 0 kW
  • Bob consumption: 6 kW → Local: 12 × (6/12) = 6.0 kW, Grid: 0 kW

Period totals:

  • Alice: 10 kWh local, 4 kWh grid (14 kWh total consumption)
  • Bob: 12 kWh local, 6 kWh grid (18 kWh total consumption)

With tariffs:

  • Local: 0.10 CHF/kWh
  • Grid: 0.30 CHF/kWh

Alice's energy charges:

  • Local: 10 × 0.10 = CHF 1.00
  • Grid: 4 × 0.30 = CHF 1.20
  • Subtotal: CHF 2.20

Questions?

See FAQ and Glossary for more on energy allocation and billing concepts.