Simplify status interface
Deploy Solar Relay / deploy (push) Successful in 1m2s

This commit is contained in:
franzz committed 2026-11-02 23:14:13 +01:00
1 parent cd7ddd3cf6
commit 89621bc7c3
15 files changed
+289 -74

No files matched your search

+88 -5
View File
@@ -42,8 +42,56 @@ class CoverageTests(unittest.TestCase):
self.assertEqual(yesterday["expected"], 288)
self.assertEqual(yesterday["complete"], 288)
self.assertEqual(yesterday["status"], "complete")
self.assertEqual(result[0]["elapsed"], 0)
self.assertTrue(all(mask == -1 for mask in result[0]["slots"]))
self.assertEqual(result[0]["elapsed"], 1)
self.assertEqual(result[0]["slots"][0], 3)
self.assertTrue(all(mask == -1 for mask in result[0]["slots"][1:]))
def test_successful_2305_extraction_is_green_at_2308(self):
zone = ZoneInfo("Europe/Zurich")
now = datetime(2026, 11, 2, 23, 8, tzinfo=zone)
stamp = round(now.replace(minute=5).timestamp() * 1000)
day = daily_coverage([(stamp, stamp, 1, stamp, 1, stamp, 1)], zone.key, 1, now)[0]
self.assertEqual(day["slots"][277], 3)
self.assertEqual(day["slots"][278], -1)
self.assertEqual(day["complete"], 1)
self.assertEqual(day["energy_complete"], 1)
self.assertEqual(day["boiler_complete"], 1)
self.assertEqual(day["elapsed"], 278)
self.assertEqual(day["missing"], 277)
def test_current_slot_without_extraction_stays_pending_until_it_ends(self):
zone = ZoneInfo("Europe/Zurich")
now = datetime(2026, 11, 2, 23, 8, tzinfo=zone)
day = daily_coverage([], zone.key, 1, now)[0]
self.assertEqual(day["slots"][277], -1)
self.assertEqual(day["elapsed"], 277)
self.assertEqual(day["missing"], 277)
after = daily_coverage([], zone.key, 1, now.replace(minute=10))[0]
self.assertEqual(after["slots"][277], 0)
self.assertEqual(after["slots"][278], -1)
self.assertEqual(after["missing"], 278)
def test_partial_and_failed_current_extractions_are_shown_immediately(self):
zone = ZoneInfo("Europe/Zurich")
now = datetime(2026, 11, 2, 23, 8, tzinfo=zone)
stamp = round(now.replace(minute=5).timestamp() * 1000)
for energy, boiler, expected_mask in [(1, 0, 1), (0, 1, 2), (0, 0, 0)]:
with self.subTest(energy=energy, boiler=boiler):
day = daily_coverage([(stamp, stamp, energy, stamp, 0, stamp, boiler)], zone.key, 1, now)[0]
self.assertEqual(day["slots"][277], expected_mask)
self.assertEqual(day["elapsed"], 278)
self.assertEqual(day["complete"], 0)
def test_current_extraction_at_midnight_is_counted_without_waiting(self):
zone = ZoneInfo("Europe/Zurich")
now = datetime(2026, 11, 2, 0, 0, 10, tzinfo=zone)
stamp = round(now.replace(second=0).timestamp() * 1000)
day = daily_coverage([(stamp, stamp, 1, stamp, 0, stamp, 1)], zone.key, 1, now)[0]
self.assertEqual(day["slots"][0], 3)
self.assertEqual(day["elapsed"], 1)
self.assertEqual(day["complete"], 1)
self.assertEqual(day["missing"], 0)
self.assertTrue(all(mask == -1 for mask in day["slots"][1:]))
def test_dst_days_expect_276_and_300_intervals(self):
for date, expected in [("2026-03-29", 276), ("2026-10-25", 300)]:
@@ -61,17 +109,27 @@ class CoverageTests(unittest.TestCase):
self.assertEqual(yesterday["missing"], 3)
self.assertEqual(yesterday["status"], "partial")
def test_energy_failure_does_not_erase_meter_or_boiler_coverage(self):
def test_energy_failure_does_not_erase_boiler_coverage(self):
rows, end = rows_for_day("2026-10-30")
stamp = rows[1][0]
rows[1] = (stamp, stamp, 0, stamp, 1, stamp, 1)
yesterday = daily_coverage(rows, "Europe/Zurich", 1, end + timedelta(minutes=3))[1]
self.assertEqual(yesterday["slots"][1], 6)
self.assertEqual(yesterday["slots"][1], 2)
self.assertEqual(yesterday["energy_complete"], 287)
self.assertEqual(yesterday["meter_complete"], 288)
self.assertNotIn("meter_complete", yesterday)
self.assertEqual(yesterday["boiler_complete"], 288)
self.assertEqual(yesterday["missing"], 1)
def test_missing_meter_readings_do_not_affect_visible_completeness(self):
rows, end = rows_for_day("2026-10-30")
rows = [(observed, power_ms, power_ok, meter_ms, 0, boiler_ms, boiler_ok)
for observed, power_ms, power_ok, meter_ms, meter_ok, boiler_ms, boiler_ok in rows]
yesterday = daily_coverage(rows, "Europe/Zurich", 1, end + timedelta(minutes=3))[1]
self.assertEqual(yesterday["complete"], 288)
self.assertEqual(yesterday["status"], "complete")
self.assertEqual(set(yesterday["slots"]), {3})
self.assertNotIn("meter_complete", yesterday)
def test_source_errors_stale_readings_and_false_boiler_state(self):
when = datetime(2026, 10, 30, tzinfo=timezone.utc)
value = observed(when)
@@ -150,6 +208,31 @@ class StatusHttpTests(unittest.TestCase):
self.assertEqual(result["collector"]["source_errors"], ["boiler"])
self.assertNotIn("secret canary", json.dumps(result))
def test_meter_failure_does_not_change_current_status_or_last_success(self):
value = observed(datetime.now(timezone.utc))
value["errors"] = {"meter": "meter is unavailable"}
value["readings"]["grid_import_total_wh"] = None
value["readings"]["grid_export_total_wh"] = None
self.store.add(value)
result = json.loads(self.get("/api/status")[2])
self.assertEqual(result["status"], "ok")
self.assertEqual(result["collector"]["source_errors"], [])
self.assertEqual(result["collector"]["last_success"], value["observed_at"])
self.assertNotIn("meter_complete", result["days"][0])
def test_latest_success_and_current_grid_cell_agree(self):
zone = ZoneInfo("Europe/Zurich")
now = datetime(2026, 11, 2, 23, 8, tzinfo=zone)
value = observed(now.replace(minute=5))
self.store.add(value)
with unittest.mock.patch("solar_relay.status.datetime", wraps=datetime) as dates:
dates.now.return_value = now.astimezone(timezone.utc)
result = json.loads(self.get("/api/status?days=1")[2])
self.assertEqual(result["status"], "ok")
self.assertEqual(result["collector"]["last_success"], value["observed_at"])
self.assertEqual(result["days"][0]["slots"][277], 3)
self.assertEqual(result["days"][0]["slots"][278], -1)
def test_only_explicit_read_only_routes_are_available(self):
for path in ["/config.toml", "/.env", "/data/archive/date=2026-11-01/samples.parquet", "/../config.toml", "/solar-relay/.git/config"]:
self.assertEqual(self.get(path)[0], 404, path)