Initial commit
Deploy Solar Relay / deploy (push) Failing after 4s

This commit is contained in:
franzz committed 2026-11-01 22:33:32 +01:00
1 parent 36180dffd7
commit ec75ac15b0
30 files changed
+3042 -1

No files matched your search

+88
View File
@@ -0,0 +1,88 @@
"""Five-minute extraction completeness, distinct from continuous observed coverage."""
from collections import Counter
from datetime import datetime, timedelta, timezone
import math
from zoneinfo import ZoneInfo
from .config import utc_text
BUCKET_MS = 300_000
MAX_SOURCE_AGE_MS = 90_000
def milliseconds(value):
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
if parsed.tzinfo is None:
raise ValueError("Observation timestamp has no timezone")
return round(parsed.timestamp() * 1000)
def _finite(value):
return not isinstance(value, bool) and isinstance(value, (int, float)) and math.isfinite(value)
def sample_coverage(sample):
observed = milliseconds(sample["observed_at"])
if "history_interval" in sample:
# Cloud history has measured interval energy, but no cumulative meter or relay history.
return (sample["id"], sample["source_id"], observed, observed, 1, observed, 0, observed, 0)
readings = sample.get("readings", {})
result = [sample["id"], sample["source_id"], observed]
for source, fields in [
("powerflow", ["production_ac_w", "consumption_w", "grid_import_w", "grid_export_w"]),
("meter", ["grid_import_total_wh", "grid_export_total_wh"]),
("boiler", []),
]:
try:
stamp = milliseconds(sample["source_timestamps"][source])
valid = source not in sample.get("errors", {}) and abs(stamp - observed) <= MAX_SOURCE_AGE_MS
except (KeyError, ValueError, TypeError, AttributeError):
stamp, valid = observed, False
valid = valid and all(_finite(readings.get(field)) for field in fields)
if source == "boiler":
valid = valid and isinstance(readings.get("boiler_enabled"), bool)
result.extend([stamp if valid else observed, int(valid)])
return tuple(result)
def daily_coverage(rows, timezone_name, days, now=None):
now = now or datetime.now(timezone.utc)
zone = ZoneInfo(timezone_name)
today = now.astimezone(zone).date()
first = today - timedelta(days=days)
upper = round(now.timestamp() * 1000)
slots_by_time = {}
observations = Counter()
for row in rows:
observed = row[0]
if observed > upper + 1000:
continue
date = datetime.fromtimestamp(observed / 1000, zone).date().isoformat()
observations[date] += 1
bucket = observed // BUCKET_MS * BUCKET_MS
mask = sum(1 << index for index in range(3) if row[2 + index * 2])
slots_by_time[bucket] = slots_by_time.get(bucket, 0) | mask
result = []
for offset in range(days + 1):
date = today - timedelta(days=offset)
start = datetime.combine(date, datetime.min.time(), zone)
end = datetime.combine(date + timedelta(days=1), datetime.min.time(), zone)
start_ms = round(start.timestamp() * 1000)
expected = round((end.timestamp() - start.timestamp()) * 1000) // BUCKET_MS
elapsed = expected if date < today else max(0, min(expected, (upper - start_ms) // BUCKET_MS))
slots = []
for index in range(expected):
slots.append(slots_by_time.get(start_ms + index * BUCKET_MS, 0) if index < elapsed else -1)
complete = sum(mask == 7 for mask in slots)
count = observations[date.isoformat()]
status = "in_progress" if date == today else "complete" if complete == expected else "partial" if count else "missing"
result.append({
"date": date.isoformat(), "day_start": utc_text(start), "expected": expected, "elapsed": elapsed,
"complete": complete, "missing": elapsed - complete, "samples": count, "status": status,
"energy_complete": sum(mask >= 0 and bool(mask & 1) for mask in slots),
"meter_complete": sum(mask >= 0 and bool(mask & 2) for mask in slots),
"boiler_complete": sum(mask >= 0 and bool(mask & 4) for mask in slots),
"slots": slots,
})
return result