Skip to content

Commit 3d0c0fe

Browse files
committed
fix(#303): address PR review comments
- Normalize trailing 'Z' in ISO timestamps before fromisoformat() for Python 3.9/3.10 compatibility - Use floor division (// 3600) for hour_offset so negative deltas between -1h and 0h stay negative and get skipped correctly - Scan all forecast list entries for expected keys instead of only checking the first element (avoids skipping valid data if first entry is malformed) - Cache _base_hour_start in _hour_str() helper to prevent flaky tests near hour boundaries - Fix misleading docstring in test_auto_detect_none_unit_defaults_to_wh
1 parent 2f7bb9e commit 3d0c0fe

2 files changed

Lines changed: 25 additions & 11 deletions

File tree

src/batcontrol/forecastsolar/forecast_homeassistant_ml.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -472,8 +472,11 @@ def _parse_forecast_from_attributes(
472472
# Format 1: forecast list with {start, end, value} - evcc Solar Forecast style
473473
forecast_list = attributes.get("forecast")
474474
if forecast_list and isinstance(forecast_list, list):
475-
first = forecast_list[0] if forecast_list else {}
476-
if isinstance(first, dict) and "start" in first and "value" in first:
475+
has_expected_entry = any(
476+
isinstance(entry, dict) and "start" in entry and "value" in entry
477+
for entry in forecast_list
478+
)
479+
if has_expected_entry:
477480
logger_ha_details.debug(
478481
"Parsing forecast from 'forecast' list (%d entries)",
479482
len(forecast_list)
@@ -492,16 +495,20 @@ def _parse_forecast_from_attributes(
492495
continue
493496

494497
try:
495-
entry_start = datetime.datetime.fromisoformat(
496-
start_str)
498+
# Normalize UTC timestamps with trailing 'Z' for Python 3.9/3.10
499+
if isinstance(start_str, str) and start_str.endswith("Z"):
500+
start_str = start_str[:-1] + "+00:00"
501+
502+
entry_start = datetime.datetime.fromisoformat(start_str)
497503
# If the timestamp is naive, assume it is in the local timezone
498504
if entry_start.tzinfo is None:
499505
entry_start = self.timezone.localize(entry_start)
500506
else:
501507
entry_start = entry_start.astimezone(self.timezone)
502508

503509
delta = entry_start - current_hour
504-
hour_offset = int(delta.total_seconds() / 3600)
510+
# Use floor division so negative deltas stay negative
511+
hour_offset = int(delta.total_seconds() // 3600)
505512

506513
if hour_offset < 0:
507514
# Past hour - skip

tests/test_forecast_solar_homeassistant_ml.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -572,10 +572,16 @@ def _make_provider_wh(self, pv_installations, timezone):
572572
)
573573

574574
def _hour_str(self, tz, offset_hours: int) -> str:
575-
"""Return an ISO timestamp string for current-hour + offset_hours in local tz."""
576-
now = datetime.datetime.now(tz)
577-
hour_start = now.replace(minute=0, second=0, microsecond=0)
578-
target = hour_start + datetime.timedelta(hours=offset_hours)
575+
"""Return an ISO timestamp string for current-hour + offset_hours in local tz.
576+
577+
Caches a single base-hour per instance to avoid flakiness when tests
578+
run near an hour boundary (successive calls to datetime.now() could
579+
return different hours otherwise).
580+
"""
581+
if not hasattr(self, "_base_hour_start"):
582+
now = datetime.datetime.now(tz)
583+
self._base_hour_start = now.replace(minute=0, second=0, microsecond=0)
584+
target = self._base_hour_start + datetime.timedelta(hours=offset_hours)
579585
# Return naive local time (as the sensor provides)
580586
return target.strftime("%Y-%m-%dT%H:%M:%S")
581587

@@ -733,8 +739,9 @@ def test_auto_detect_none_unit_defaults_to_wh(self, pv_installations, timezone):
733739
}
734740
}
735741

736-
# Build a provider with explicit Wh unit, then directly test the async
737-
# unit-check by calling _check_sensor_unit_async with a mocked WebSocket.
742+
# Build a provider with explicit Wh unit, then directly exercise
743+
# _check_sensor_unit_async() to verify that a None unit_of_measurement
744+
# is handled gracefully (returns 1.0 with a warning instead of raising).
738745
provider = ForecastSolarHomeAssistantML(
739746
pvinstallations=pv_installations,
740747
timezone=timezone,

0 commit comments

Comments
 (0)