fix(energy): report real kWh in the cost Summary instead of mislabelled money
frontend / frontend (push) Successful in 10m24s
pytest / test (push) Successful in 11m57s
docker-image / build-and-push (push) Successful in 1m37s

The Summary cards labelled `metered_import` / `metered_export` as "(kWh)", but
both fields are monetary totals (Σ import_cost / Σ export_revenue).  Today's
page therefore showed "Import 1.339 kWh" when the meter had actually imported
4.188 kWh — the 1.339 was EUR.  Cross-checked against the DSMR cumulative
registers and Home Assistant: our energy figures were correct all along, only
the label was wrong.

summarize() now also aggregates the metered energy, reusing the already-fetched
non-degraded rows so no extra query is issued:

  metered_import_kwh = Σ (d1_kwh + d2_kwh)
  metered_export_kwh = Σ (r1_kwh + r2_kwh)

The Import/Export cards show kWh as the headline figure and keep the monetary
equivalent as a sub-line, so the split between energy cost and standing
charges/credits behind total_payable stays visible.

The `_kwh` suffix is now the only thing separating energy from money in this
payload, so the docstrings on both summarize() and SummaryResponse call that
out explicitly.

app/integrations/expose.py reads only the money keys, so the HA outbound
sensors are unaffected by the additive fields.
This commit is contained in:
2026-08-06 22:20:56 +02:00
parent b405aea88b
commit 0958d9a2e9
9 changed files with 157 additions and 31 deletions
+16 -4
View File
@@ -2220,7 +2220,9 @@ export interface components {
* SummaryResponse
* @description Response for GET /api/energy/costs/summary.
*
* All monetary values are in ``currency``.
* Monetary values are in ``currency``; the ``*_kwh`` fields are energy totals
* in kWh. ``metered_import``/``metered_export`` are **money**, not energy —
* only the ``_kwh``-suffixed fields carry kWh.
*
* ``total_payable = metered_net + fixed_costs credits``
*/
@@ -2229,19 +2231,29 @@ export interface components {
currency: string;
/**
* Metered Import
* @description Σ import_cost for non-degraded periods.
* @description Σ import_cost for non-degraded periods (money, in `currency`).
*/
metered_import: number;
/**
* Metered Export
* @description Σ export_revenue for non-degraded periods.
* @description Σ export_revenue for non-degraded periods (money, in `currency`).
*/
metered_export: number;
/**
* Metered Net
* @description Σ net_cost for non-degraded periods.
* @description Σ net_cost for non-degraded periods (money, in `currency`).
*/
metered_net: number;
/**
* Metered Import Kwh
* @description Σ (d1_kwh + d2_kwh) for non-degraded periods (energy imported, kWh).
*/
metered_import_kwh: number;
/**
* Metered Export Kwh
* @description Σ (r1_kwh + r2_kwh) for non-degraded periods (energy exported, kWh).
*/
metered_export_kwh: number;
/**
* Fixed Costs
* @description Standing charges (network_fee + management_fee) apportioned over the interval.
+11 -2
View File
@@ -61,9 +61,13 @@ const COST_PERIOD = {
const SUMMARY = {
currency: 'EUR',
// Money totals and kWh totals are deliberately distinct so the assertions
// below prove the cards read the *_kwh fields, not the monetary ones.
metered_import: 10.5,
metered_export: 2.3,
metered_net: 8.2,
metered_import_kwh: 33.3,
metered_export_kwh: 44.4,
fixed_costs: 5.0,
credits: 50.0,
total_payable: 12.5,
@@ -145,9 +149,14 @@ describe('CostView', () => {
expect(screen.getByTestId('summary-import')).toBeInTheDocument()
})
expect(screen.getByTestId('summary-import')).toHaveTextContent('10.500')
expect(screen.getByTestId('summary-export')).toHaveTextContent('2.300')
// Main figure is energy (kWh), taken from the *_kwh fields.
expect(screen.getByTestId('summary-import')).toHaveTextContent('33.300')
expect(screen.getByTestId('summary-export')).toHaveTextContent('44.400')
expect(screen.getByTestId('summary-total')).toHaveTextContent('12.50')
// Sub-line carries the monetary equivalent, so money is still visible.
expect(screen.getByTestId('summary-import-sub')).toHaveTextContent('10.50 EUR')
expect(screen.getByTestId('summary-export-sub')).toHaveTextContent('2.30 EUR')
})
it('shows recompute confirmation modal on button click', async () => {
+12 -3
View File
@@ -77,10 +77,12 @@ function getThisMonthRange(): { start: string; end: string } {
interface SummaryCardProps {
label: string
value: string
/** Optional secondary line, e.g. the monetary equivalent of an energy figure. */
sub?: string
testId?: string
}
function SummaryCard({ label, value, testId }: SummaryCardProps) {
function SummaryCard({ label, value, sub, testId }: SummaryCardProps) {
return (
<Paper withBorder p="sm" data-testid={testId}>
<Stack gap={4}>
@@ -90,6 +92,11 @@ function SummaryCard({ label, value, testId }: SummaryCardProps) {
<Text fw={600} size="lg">
{value}
</Text>
{sub !== undefined && (
<Text size="xs" c="dimmed" data-testid={testId ? `${testId}-sub` : undefined}>
{sub}
</Text>
)}
</Stack>
</Paper>
)
@@ -207,12 +214,14 @@ export function CostView() {
<SimpleGrid cols={{ base: 2, sm: 3 }} spacing="sm" data-testid="cost-summary">
<SummaryCard
label="Import (kWh)"
value={summaryQuery.data.metered_import.toFixed(3)}
value={summaryQuery.data.metered_import_kwh.toFixed(3)}
sub={`${summaryQuery.data.metered_import.toFixed(2)} ${currency}`}
testId="summary-import"
/>
<SummaryCard
label="Export (kWh)"
value={summaryQuery.data.metered_export.toFixed(3)}
value={summaryQuery.data.metered_export_kwh.toFixed(3)}
sub={`${summaryQuery.data.metered_export.toFixed(2)} ${currency}`}
testId="summary-export"
/>
<SummaryCard
@@ -83,6 +83,8 @@ const SUMMARY = {
metered_import: 10.5,
metered_export: 2.3,
metered_net: 8.2,
metered_import_kwh: 33.3,
metered_export_kwh: 44.4,
fixed_costs: 5.0,
credits: 50.0,
total_payable: 12.5,