Fix range_days undercounting for the "all time" battery range
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 18s

(end - start).days truncates instead of rounding, so "all time" (which
starts at the earliest reading's exact timestamp, rarely local midnight)
could report fewer range_days than days_with_data -- reads backwards
("5 of 4 days have data"). Round the duration up instead; day-aligned
presets and custom ranges are unaffected since they're already exact
multiples of 24h.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
dlawler489 2026-09-13 11:13:19 +10:00
parent c30165c153
commit 591da31e60

View file

@ -1,5 +1,6 @@
from __future__ import annotations
import math
from collections import defaultdict
from datetime import datetime, timedelta, timezone
from zoneinfo import ZoneInfo
@ -54,6 +55,19 @@ def _resolve_range(
return _resolve_start(session, days, end), end
def _range_days(start: datetime, end: datetime) -> int:
"""Number of calendar days a [start, end) range could span. Rounds up
(not truncates) the raw duration, since `start` isn't always exactly at
a day boundary -- "all time" starts at the first reading's actual
timestamp, which is very rarely local midnight -- and truncating there
would make `days_with_data` (a whole-calendar-day count) look bigger
than the range it's supposedly a fraction of. A day-aligned preset
(`days=N`, or a custom Y-M-D range) already lands on an exact multiple
of 24h, so rounding up is a no-op for those and only changes the
"all time" case."""
return max(1, math.ceil((end - start).total_seconds() / 86400))
def _bucket_key(local_date, granularity: str) -> str:
if granularity == "month":
return local_date.replace(day=1).isoformat()
@ -215,7 +229,7 @@ def battery_history(
if row.battery_soc_percent is not None
]
days_with_data = len({row.ts.astimezone(BNE).date() for row in rows if row.battery_soc_percent is not None})
range_days = max(1, (end - start).days)
range_days = _range_days(start, end)
points = all_points
if len(points) > _MAX_BATTERY_CHART_POINTS:
@ -245,7 +259,7 @@ def battery_value(
rows = get_usage_series(session, start, end)
current_plan = get_current_plan(session)
range_days = max(1, (end - start).days)
range_days = _range_days(start, end)
battery_rows = [r for r in rows if r.battery_charge_w is not None or r.battery_discharge_w is not None]
if not battery_rows or current_plan is None:
return {"available": False, "range_days": range_days}