1+ import json
12import sqlite3
23from datetime import datetime , timedelta , timezone
34from types import SimpleNamespace
@@ -65,7 +66,7 @@ def daily_row(path, date_key):
6566 ).fetchone ()
6667
6768
68- def insert_daily (path , date_key , wh , savings = 0 , peak = 0 ):
69+ def insert_daily (path , date_key , wh , savings = 0 , peak = 0 , sample_count = 1 ):
6970 with sqlite3 .connect (path ) as con :
7071 con .execute (
7172 """
@@ -79,9 +80,16 @@ def insert_daily(path, date_key, wh, savings=0, peak=0):
7980 sample_count,
8081 updated_at
8182 )
82- VALUES(?, ?, ?, 0.35, 'EUR', ?, 1 , ?)
83+ VALUES(?, ?, ?, 0.35, 'EUR', ?, ? , ?)
8384 """ ,
84- (date_key , wh , savings , peak , f"{ date_key } T12:00:00+00:00" ),
85+ (
86+ date_key ,
87+ wh ,
88+ savings ,
89+ peak ,
90+ sample_count ,
91+ f"{ date_key } T12:00:00+00:00" ,
92+ ),
8593 )
8694
8795
@@ -96,6 +104,35 @@ def test_store_records_latest_and_history(tmp_path):
96104 assert store .latest ()["control_explain" ] is None
97105
98106
107+ def test_latest_refreshes_energy_stats_from_daily_aggregates (tmp_path ):
108+ path = tmp_path / "dashboard.sqlite"
109+ DashboardStore (path )
110+ insert_daily (path , "2026-05-31" , 1000 , savings = 1 )
111+ timestamp = datetime (2026 , 6 , 1 , 12 , 0 , tzinfo = timezone .utc ).isoformat ()
112+
113+ with sqlite3 .connect (path ) as con :
114+ stale_snapshot = snapshot (timestamp )
115+ stale_snapshot ["energy_stats" ] = {
116+ "enabled" : True ,
117+ "currency" : "EUR" ,
118+ "lifetime" : {
119+ "inverter_output_wh" : 0 ,
120+ "inverter_output_kwh" : 0 ,
121+ "savings_value" : 0 ,
122+ },
123+ }
124+ con .execute (
125+ "INSERT INTO snapshots(timestamp, payload) VALUES(?, ?)" ,
126+ (timestamp , json .dumps (stale_snapshot )),
127+ )
128+
129+ store = DashboardStore (path )
130+ latest = store .latest ()
131+
132+ assert latest ["energy_stats" ]["lifetime" ]["inverter_output_wh" ] == 1000
133+ assert latest ["energy_stats" ]["lifetime" ]["since_date" ] == "2026-05-31"
134+
135+
99136def test_store_cleanup_uses_retention (tmp_path ):
100137 store = DashboardStore (tmp_path / "dashboard.sqlite" , retention_hours = 1 )
101138 old = (datetime .now (timezone .utc ) - timedelta (hours = 3 )).isoformat ()
@@ -287,10 +324,53 @@ def test_energy_rolling_summaries_and_best_day(tmp_path):
287324 assert summary ["last_4_weeks" ]["inverter_output_wh" ] == 10000
288325 assert summary ["last_12_months" ]["inverter_output_wh" ] == 15000
289326 assert summary ["lifetime" ]["inverter_output_wh" ] == 21000
327+ assert summary ["lifetime" ]["since_date" ] == "2025-06-29"
290328 assert summary ["best_day" ]["date" ] == "2025-06-29"
291329 assert summary ["best_day" ]["inverter_output_wh" ] == 6000
292330
293331
332+ def test_energy_lifetime_since_date_is_null_without_daily_stats (tmp_path ):
333+ store = DashboardStore (tmp_path / "dashboard.sqlite" )
334+
335+ summary = store .energy_summary (
336+ now = datetime (2026 , 6 , 29 , 12 , 0 , tzinfo = timezone .utc )
337+ )
338+
339+ assert summary ["lifetime" ]["inverter_output_wh" ] == 0
340+ assert summary ["lifetime" ]["since_date" ] is None
341+
342+
343+ def test_energy_lifetime_since_date_uses_first_collected_day_across_gaps (tmp_path ):
344+ path = tmp_path / "dashboard.sqlite"
345+ DashboardStore (path )
346+ insert_daily (path , "2026-06-01" , 1000 , savings = 1 )
347+ insert_daily (path , "2026-06-08" , 2000 , savings = 2 )
348+
349+ store = DashboardStore (path )
350+ summary = store .energy_summary (
351+ now = datetime (2026 , 6 , 29 , 12 , 0 , tzinfo = timezone .utc )
352+ )
353+
354+ assert summary ["lifetime" ]["inverter_output_wh" ] == 3000
355+ assert summary ["lifetime" ]["since_date" ] == "2026-06-01"
356+
357+
358+ def test_energy_lifetime_since_date_ignores_zero_sample_days (tmp_path ):
359+ path = tmp_path / "dashboard.sqlite"
360+ DashboardStore (path )
361+ insert_daily (path , "2026-05-31" , 5000 , savings = 5 , sample_count = 0 )
362+ insert_daily (path , "2026-06-01" , 1000 , savings = 1 )
363+ insert_daily (path , "2026-06-02" , 2000 , savings = 2 )
364+
365+ store = DashboardStore (path )
366+ summary = store .energy_summary (
367+ now = datetime (2026 , 6 , 29 , 12 , 0 , tzinfo = timezone .utc )
368+ )
369+
370+ assert summary ["lifetime" ]["inverter_output_wh" ] == 3000
371+ assert summary ["lifetime" ]["since_date" ] == "2026-06-01"
372+
373+
294374def test_energy_monthly_and_yearly_summaries (tmp_path ):
295375 path = tmp_path / "dashboard.sqlite"
296376 DashboardStore (path )
0 commit comments