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
- Consumption (
- 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):
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:
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:
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 (
OUTreadings)
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 periodyearly_fee: monthly installment (fixed_price_chf / 12) per intersecting monthper_metering_point_monthly_fee: charged per active metering point per intersecting monthper_metering_point_yearly_fee: monthly installment (fixed_price_chf / 12) per active metering point per intersecting monthshared_monthly_fee:fixed_price_chfdivided by the number of participants active in each intersecting monthshared_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:
- 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.
- 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:
- Period check: Invoice period matches expected billing window ✓
- Energy split check: Local + Grid = Total consumption (minus feed-in) ✓
- Tariff check: Line item prices match published tariffs ✓
- Credit check: Feed-in and other credits appear as negatives ✓
- Rounding check: Line totals and VAT are CHF cents ✓
- 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.