diff --git a/apps/predbat/fetch.py b/apps/predbat/fetch.py index 4485c6841..d34065850 100644 --- a/apps/predbat/fetch.py +++ b/apps/predbat/fetch.py @@ -1989,6 +1989,11 @@ def fetch_ml_load_forecast(self, now_utc): and returns it as a minute_data dictionary """ # Use ML Model for load prediction + status = self.get_state_wrapper("sensor." + self.prefix + "_load_ml_forecast") + if status and status != "active": + self.log("ML load forecast is not active (status: {}), falling back to historical data".format(status)) + return {} + load_ml_forecast = self.get_state_wrapper("sensor." + self.prefix + "_load_ml_forecast", attribute="results") if load_ml_forecast: self.log("Loading ML load forecast from sensor.{}_load_ml_forecast".format(self.prefix)) diff --git a/apps/predbat/load_ml_component.py b/apps/predbat/load_ml_component.py index 859e23836..dbde088cc 100644 --- a/apps/predbat/load_ml_component.py +++ b/apps/predbat/load_ml_component.py @@ -23,7 +23,7 @@ import os from datetime import datetime, timezone, timedelta from component_base import ComponentBase -from utils import get_now_from_cumulative, dp2, dp4, minute_data +from utils import get_now_from_cumulative, dp2, dp4, minute_data, safe_float from load_predictor import LoadPredictor, MODEL_VERSION from const import TIME_FORMAT, PREDICT_STEP import json @@ -1054,9 +1054,10 @@ def _publish_entity(self): }, app="load_ml", ) + self.dashboard_item( "sensor." + self.prefix + "_load_ml_stats", - state=round(total_kwh, 2), + state=safe_float(round(total_kwh, 2)), attributes={ "load_today": dp2(self.load_minutes_now), "load_today_h1": dp2(load_today_h1), @@ -1065,10 +1066,10 @@ def _publish_entity(self): "power_today_now": dp2(power_today_now), "power_today_h1": dp2(power_today_h1), "power_today_h8": dp2(power_today_h8), - "mae_kwh": round(float(self.predictor.validation_mae), 4) if self.predictor and self.predictor.validation_mae is not None else None, - "bias_kwh": round(self.predictor.validation_bias, 4) if self.predictor and self.predictor.validation_bias is not None else None, + "mae_kwh": safe_float(self.predictor.validation_mae) if self.predictor else None, + "bias_kwh": safe_float(self.predictor.validation_bias) if self.predictor else None, "last_trained": self.last_train_time.isoformat() if self.last_train_time else None, - "model_age_hours": round(model_age_hours, 1) if model_age_hours is not None else None, + "model_age_hours": safe_float(model_age_hours), "training_days": self.load_data_age_days, "status": self.model_status, "model_version": MODEL_VERSION, diff --git a/apps/predbat/load_predictor.py b/apps/predbat/load_predictor.py index 25bf6cc85..a345d9b07 100644 --- a/apps/predbat/load_predictor.py +++ b/apps/predbat/load_predictor.py @@ -1345,7 +1345,7 @@ def train_curriculum( ) total_passes = len(window_sizes) + 1 # intermediate passes + final full pass - self.log("ML Predictor: Curriculum training - {} passes, window {:.1f}→{:.1f} days + final full pass ({:.1f} days)".format(total_passes, window_sizes[0] / day_minutes, window_sizes[-1] / day_minutes, max_minute / day_minutes)) + self.log("ML Predictor: Curriculum training - {} passes, window {:.1f}->{:.1f} days + final full pass ({:.1f} days)".format(total_passes, window_sizes[0] / day_minutes, window_sizes[-1] / day_minutes, max_minute / day_minutes)) val_mae = None for pass_idx, window in enumerate(window_sizes): diff --git a/apps/predbat/predbat.py b/apps/predbat/predbat.py index 7cf8dc21d..03bbdcaf3 100644 --- a/apps/predbat/predbat.py +++ b/apps/predbat/predbat.py @@ -354,6 +354,10 @@ def reset(self): self.max_days_previous = max(self.days_previous) + 1 self.forecast_days = 0 self.forecast_minutes = 0 + self.inverter_clock_skew_start = 0 + self.inverter_clock_skew_end = 0 + self.inverter_clock_skew_discharge_start = 0 + self.inverter_clock_skew_discharge_end = 0 self.soc_kw = 0 self.soc_percent = 0 self.soc_max = 10.0 @@ -1602,7 +1606,7 @@ def initialize(self): self.record_status("Error: Some components failed to start (phase 2)", had_errors=True) self.load_user_config(quiet=False, register=True) - self.auto_config(final=True) + self.auto_config(final=False) self.validate_config() # Restore the last saved plan so it is immediately active before the first calculation @@ -1770,6 +1774,12 @@ def run_time_loop(self, cb_args): self.validate_config() config_changed = True + # Retry auto config if we have unmatched regexes + if hasattr(self, "unmatched_args") and len(self.unmatched_args) > 0: + # Give HA 10 minutes from startup to fully load entities before final disable + time_since_start = (self.now_utc_real - self.started_time).total_seconds() / 60.0 + self.auto_config(final=(time_since_start > 10)) + # Run the prediction self.update_pred(scheduled=True) diff --git a/apps/predbat/tests/open_meteo_live.py b/apps/predbat/tests/open_meteo_live.py index 3ac6ff669..e69989ae3 100644 --- a/apps/predbat/tests/open_meteo_live.py +++ b/apps/predbat/tests/open_meteo_live.py @@ -159,7 +159,7 @@ def print_comparison_table(label: str, sources: dict, day_str: str, tz_name: str """Print a side-by-side comparison table for one day. sources is an ordered dict of {source_name: hourly_dict} where each hourly_dict - maps 'YYYY-MM-DDTHH' → (kwh_p50, kwh_p10) as returned by _aggregate_to_hourly(). + maps 'YYYY-MM-DDTHH' -> (kwh_p50, kwh_p10) as returned by _aggregate_to_hourly(). Hours are shown in local time (tz_name). """ source_names = list(sources.keys()) @@ -211,8 +211,8 @@ def print_comparison_table(label: str, sources: dict, day_str: str, tz_name: str async def run(fs_api_key: str = None, solcast_api_key: str = None, solcast_host: str = None) -> None: """Fetch live data via fetch_pv_forecast() for each source and print comparison. - Runs the full Predbat PV fetch pipeline (download → minute_data → pv_calibration - → publish_pv_stats) for each enabled source, then reads the detailedForecast that + Runs the full Predbat PV fetch pipeline (download -> minute_data -> pv_calibration + -> publish_pv_stats) for each enabled source, then reads the detailedForecast that Predbat would store in sensor.predbat_pv_today. """ now_local = datetime.now(tz=LOCAL_TZ) @@ -228,10 +228,10 @@ async def run(fs_api_key: str = None, solcast_api_key: str = None, solcast_host: _tmp = SolarAPI.__new__(SolarAPI) for i, a in enumerate(ARRAYS, 1): az_api = SolarAPI.convert_azimuth(_tmp, a["azimuth"]) - print(f" [{i}] postcode={a['postcode']} kwp={a['kwp']} declination={a['declination']}° azimuth={a['azimuth']}° (→API: {az_api:.0f}°) efficiency={a.get('efficiency', 1.0):.0%}") + print(f" [{i}] postcode={a['postcode']} kwp={a['kwp']} declination={a['declination']}° azimuth={a['azimuth']}° (->API: {az_api:.0f}°) efficiency={a.get('efficiency', 1.0):.0%}") print(f" cache: {_CACHE_ROOT}/cache/") - # results maps source_name → {"today": hourly_dict, "tomorrow": hourly_dict} + # results maps source_name -> {"today": hourly_dict, "tomorrow": hourly_dict} results: dict = {} # ── Open-Meteo ──────────────────────────────────────────────────────────── diff --git a/apps/predbat/tests/test_auto_config.py b/apps/predbat/tests/test_auto_config.py new file mode 100644 index 000000000..02c25ca33 --- /dev/null +++ b/apps/predbat/tests/test_auto_config.py @@ -0,0 +1,77 @@ +# ----------------------------------------------------------------------------- +# Predbat Home Battery System +# ----------------------------------------------------------------------------- + + +def run_auto_config_tests(my_predbat): + """ + Test auto_config retry logic and list/dict matching + """ + failed = False + print("\n============================================================") + print("Running auto_config tests") + print("============================================================") + + # Backup original + original_args = my_predbat.args.copy() + original_unmatched = getattr(my_predbat, "unmatched_args", {}).copy() + + try: + # Test 1: List matching retains 're:' elements if they fail + print("\n=== Test 1: List regex matching ===") + my_predbat.args = {"pv_forecast_raw": ["sensor.inv1", "re:.*inv2"]} + my_predbat.unmatched_args = {} + + # This simulates < 10 mins, so final=False + my_predbat.auto_config(final=False) + + # It should still be in args exactly as it was, because matched=True for lists + val = my_predbat.args.get("pv_forecast_raw") + if val != ["sensor.inv1", "re:.*inv2"]: + print(f"FAIL: List regex matching failed to retain string. Got {val}") + failed = True + else: + print("PASS: List regex retained.") + + # Test 2: Dict matching retains 're:' elements if they fail + print("\n=== Test 2: Dict regex matching ===") + my_predbat.args = {"my_dict": {"foo": "sensor.foo", "bar": "re:.*bar"}} + my_predbat.unmatched_args = {} + + my_predbat.auto_config(final=False) + + val = my_predbat.args.get("my_dict") + if val != {"foo": "sensor.foo", "bar": "re:.*bar"}: + print(f"FAIL: Dict regex matching failed to retain string. Got {val}") + failed = True + else: + print("PASS: Dict regex retained.") + + # Test 3: Standard unmatched arg is moved to unmatched_args after 10 mins (final=True) + print("\n=== Test 3: Final=True moves unmatched args ===") + my_predbat.args = {"inverter_limit": "re:.*missing_limit.*"} + my_predbat.unmatched_args = {} + + my_predbat.auto_config(final=True) + + if "inverter_limit" in my_predbat.args: + print(f"FAIL: inverter_limit still in args: {my_predbat.args['inverter_limit']}") + failed = True + elif "inverter_limit" not in my_predbat.unmatched_args: + print(f"FAIL: inverter_limit not in unmatched_args") + failed = True + else: + print("PASS: Final=True moved unmatched arg correctly.") + + finally: + my_predbat.args = original_args + my_predbat.unmatched_args = original_unmatched + + print("============================================================") + if failed: + print("FAIL: SOME TESTS FAILED") + else: + print("PASS: ALL TESTS PASSED") + print("============================================================") + + return failed diff --git a/apps/predbat/tests/test_axle.py b/apps/predbat/tests/test_axle.py index a1bd69b9d..a58462c70 100644 --- a/apps/predbat/tests/test_axle.py +++ b/apps/predbat/tests/test_axle.py @@ -166,13 +166,13 @@ def test_axle(my_predbat=None): try: result = test_func(my_predbat) if result: - print(f"✗ FAILED: {key}") + print(f"FAIL: FAILED: {key}") failed += 1 else: - print(f"✓ PASSED: {key}") + print(f"PASS: PASSED: {key}") passed += 1 except Exception as e: - print(f"✗ EXCEPTION in {key}: {e}") + print(f"FAIL: EXCEPTION in {key}: {e}") import traceback traceback.print_exc() @@ -202,7 +202,7 @@ def _test_axle_initialization(my_predbat=None): assert axle.event_history == [], "Event history should be empty on init" assert axle.history_loaded is False, "History should not be loaded on init" - print(" ✓ Initialisation successful") + print(" PASS: Initialisation successful") return False @@ -222,7 +222,7 @@ def _test_axle_list_api_key_validation(my_predbat=None): assert axle2.api_key is None, "Empty list should result in None api_key" - print(" ✓ List API key validation works correctly") + print(" PASS: List API key validation works correctly") return False @@ -272,7 +272,7 @@ def _test_axle_fetch_with_active_event(my_predbat=None): assert axle.failures_total == 0, "No failures should be recorded" - print(" ✓ Active event fetched and published correctly") + print(" PASS: Active event fetched and published correctly") return False @@ -315,7 +315,7 @@ def _test_axle_duplicate_event_detection(my_predbat=None): # Check that NO alert was sent for duplicate event alert_messages = [msg for msg in axle.log_messages if msg.startswith("Alert:")] assert len(alert_messages) == 0, "Should NOT send alert for duplicate event" - print(" ✓ No alert sent for duplicate event") + print(" PASS: No alert sent for duplicate event") # Reset log messages axle.log_messages = [] @@ -334,7 +334,7 @@ def _test_axle_duplicate_event_detection(my_predbat=None): assert len(alert_messages) == 1, "Should send alert for new/different event" assert "18:00" in alert_messages[0], "Alert should contain new event time" assert "20:00" in alert_messages[0], "Alert should contain new event end time" - print(" ✓ Alert sent for new/different event") + print(" PASS: Alert sent for new/different event") # Test 3: Fetch event with different end time - should trigger alert print(" Test 3: Same start but different end time") @@ -350,7 +350,7 @@ def _test_axle_duplicate_event_detection(my_predbat=None): alert_messages = [msg for msg in axle.log_messages if msg.startswith("Alert:")] assert len(alert_messages) == 1, "Should send alert for event with different end time" assert "21:00" in alert_messages[0], "Alert should contain updated end time" - print(" ✓ Alert sent for event with different end time") + print(" PASS: Alert sent for event with different end time") # Test 4: No current_event in sensor (empty state) - should trigger alert for new event print(" Test 4: Empty sensor state (first event)") @@ -374,9 +374,9 @@ def _test_axle_duplicate_event_detection(my_predbat=None): alert_messages = [msg for msg in axle.log_messages if msg.startswith("Alert:")] assert len(alert_messages) == 1, "Should send alert for first event when sensor is empty" assert "22:00" in alert_messages[0], "Alert should contain first event time" - print(" ✓ Alert sent for first event when sensor empty") + print(" PASS: Alert sent for first event when sensor empty") - print(" ✓ All duplicate event detection tests passed") + print(" PASS: All duplicate event detection tests passed") return False @@ -408,7 +408,7 @@ def _test_axle_fetch_with_notify_config(my_predbat=None): alert_messages = [msg for msg in axle.log_messages if msg.startswith("Alert:")] assert len(alert_messages) == 1, "Should send notification when set_event_notify=True" assert "18:00" in alert_messages[0], "Notification should contain event time" - print(" ✓ Notification sent when enabled") + print(" PASS: Notification sent when enabled") # Test 2: set_event_notify disabled - should NOT send notification print(" Test 2: Notifications disabled (set_event_notify=False)") @@ -430,16 +430,16 @@ def _test_axle_fetch_with_notify_config(my_predbat=None): # Verify NO notification was sent alert_messages2 = [msg for msg in axle2.log_messages if msg.startswith("Alert:")] assert len(alert_messages2) == 0, "Should NOT send notification when set_event_notify=False" - print(" ✓ Notification blocked when disabled") + print(" PASS: Notification blocked when disabled") # Verify event data was still processed correctly assert axle2.current_event["start_time"] == "2025-12-20T18:00:00+0000" assert axle2.current_event["import_export"] == "import" sensor2 = axle2.dashboard_items["binary_sensor.predbat_axle_event"] assert sensor2["state"] == "on", "Sensor should be ON even without notification" - print(" ✓ Event data processed correctly without notification") + print(" PASS: Event data processed correctly without notification") - print(" ✓ Notification config tests passed") + print(" PASS: Notification config tests passed") return False @@ -476,7 +476,7 @@ def _test_axle_fetch_with_future_event(my_predbat=None): assert len(sensor["attributes"]["event_current"]) == 1, "Should have current event in list" assert len(sensor["attributes"]["event_history"]) == 0, "Future event not in history" - print(" ✓ Future event shows OFF state correctly") + print(" PASS: Future event shows OFF state correctly") return False @@ -510,7 +510,7 @@ def _test_axle_fetch_with_past_event(my_predbat=None): assert len(sensor["attributes"]["event_current"]) == 1, "Should have current event" assert len(sensor["attributes"]["event_history"]) == 1, "Past event in history" - print(" ✓ Past event added to history correctly") + print(" PASS: Past event added to history correctly") return False @@ -542,7 +542,7 @@ def _test_axle_fetch_no_event(my_predbat=None): assert axle.failures_total == 0, "No failures for empty response" - print(" ✓ No event handled correctly") + print(" PASS: No event handled correctly") return False @@ -567,7 +567,7 @@ def _test_axle_http_error(my_predbat=None): assert axle.failures_total == 1, "Failure should be recorded for HTTP error" assert any("status code 401" in msg for msg in axle.log_messages), "Error should be logged" - print(" ✓ HTTP error handled correctly (no retry)") + print(" PASS: HTTP error handled correctly (no retry)") return False @@ -610,7 +610,7 @@ async def count_calls(self): assert any("Request failed after 3 attempts" in msg for msg in axle.log_messages), "Exception should be logged with retry count" assert any("Retrying in" in msg for msg in axle.log_messages), "Retry messages should be logged" - print(" ✓ Request exception handled correctly with 3 retries and exponential backoff") + print(" PASS: Request exception handled correctly with 3 retries and exponential backoff") return False @@ -662,7 +662,7 @@ def get_session(*args, **kwargs): assert any("Successfully fetched event data" in msg for msg in axle.log_messages), "Success should be logged" assert any("Retrying in" in msg for msg in axle.log_messages), "Retry attempts should be logged" - print(" ✓ Retry mechanism succeeds after initial failures") + print(" PASS: Retry mechanism succeeds after initial failures") return False @@ -691,7 +691,7 @@ def _test_axle_datetime_parsing_variations(my_predbat=None): assert isinstance(axle.current_event["start_time"], str), "Should be stored as string" assert isinstance(axle.current_event["end_time"], str), "Should be stored as string" - print(" ✓ Different datetime formats parsed correctly") + print(" PASS: Different datetime formats parsed correctly") return False @@ -716,7 +716,7 @@ def _test_axle_json_parse_error(my_predbat=None): assert axle.failures_total == 1, "Failure should be recorded for JSON parse error" assert any("Failed to parse JSON response" in msg for msg in axle.log_messages), "Parse error should be logged" - print(" ✓ JSON parse error handled correctly (no retry)") + print(" PASS: JSON parse error handled correctly (no retry)") return False @@ -739,7 +739,7 @@ async def mock_fetch(): assert result is True, "Run should return True" assert axle.history_loaded is True, "Should load history on first run" assert len(fetch_called) == 1, "Should fetch when updated_at is None" - print(" ✓ Fetches when no previous updated_at") + print(" PASS: Fetches when no previous updated_at") # Test 2: recent updated_at (< 10 min ago) - should NOT fetch print(" Test 2: Recent updated_at (5 min ago) - should skip") @@ -751,7 +751,7 @@ async def mock_fetch(): fetch_called.clear() result = run_async(axle2.run(seconds=300, first=False)) assert len(fetch_called) == 0, "Should not fetch when updated_at is < 10 min old" - print(" ✓ Skips fetch when data is fresh") + print(" PASS: Skips fetch when data is fresh") # Test 3: stale updated_at (>= 10 min ago) - should fetch print(" Test 3: Stale updated_at (11 min ago) - should fetch") @@ -763,7 +763,7 @@ async def mock_fetch(): fetch_called.clear() result = run_async(axle3.run(seconds=660, first=False)) assert len(fetch_called) == 1, "Should fetch when updated_at is 10+ min old" - print(" ✓ Fetches when data is stale") + print(" PASS: Fetches when data is stale") # Test 4: restart with recent updated_at (restored from storage) - should NOT fetch print(" Test 4: Restart with recent updated_at (2 min ago) - should skip") @@ -776,7 +776,7 @@ async def mock_fetch(): result = run_async(axle4.run(seconds=0, first=True)) assert axle4.history_loaded is True, "Should still load history on first run" assert len(fetch_called) == 0, "Should not fetch on restart when updated_at is still fresh" - print(" ✓ Skips fetch on restart when updated_at is still fresh") + print(" PASS: Skips fetch on restart when updated_at is still fresh") # Test 5: exactly 10 minutes - boundary: should fetch print(" Test 5: Exactly 10 minutes old - boundary fetch") @@ -788,9 +788,9 @@ async def mock_fetch(): fetch_called.clear() run_async(axle5.run(seconds=600, first=False)) assert len(fetch_called) == 1, "Should fetch at exactly 10 min boundary" - print(" ✓ Fetches at exactly 10 min boundary") + print(" PASS: Fetches at exactly 10 min boundary") - print(" ✓ Run method time-based polling logic correct") + print(" PASS: Run method time-based polling logic correct") return False @@ -896,7 +896,7 @@ def _test_axle_history_loading(my_predbat=None): assert axle.event_history[0]["import_export"] == "import" assert axle.history_loaded is True - print(" ✓ History loading filters future events correctly") + print(" PASS: History loading filters future events correctly") return False @@ -931,7 +931,7 @@ def _test_axle_history_cleanup(my_predbat=None): assert len(axle.event_history) == 1, "Should remove events older than 7 days" assert axle.event_history[0]["import_export"] == "export" - print(" ✓ History cleanup removes old events correctly") + print(" PASS: History cleanup removes old events correctly") return False @@ -1001,7 +1001,7 @@ def _test_axle_fetch_sessions(my_predbat=None): assert sessions[2]["start_time"] == "2025-12-18T14:00:00+0000" assert sessions[2]["import_export"] == "export" - print(" ✓ Fetch sessions returns current + history correctly") + print(" PASS: Fetch sessions returns current + history correctly") return False @@ -1089,12 +1089,12 @@ def get_arg(self, name, indirect=True): assert base.load_scaling_dynamic.get(end_minutes) is None, "load_scaling_dynamic after event should be unchanged" # Verify the event was logged - print(" ✓ Export rates increased by 100p/kWh for 2-hour period (14:00-16:00)") - print(f" ✓ Rate at 14:00 changed from {original_rate_before} to {base.rate_export[start_minutes]}") - print(f" ✓ Rate at 13:59 unchanged: {base.rate_export[start_minutes - 1]}") - print(f" ✓ Rate at 16:00 unchanged: {base.rate_export[end_minutes]}") - print(f" ✓ {len(rate_replicate)} minutes marked as 'saving' events") - print(f" ✓ load_scaling_saving ({base.load_scaling_saving}) applied to {len(base.load_scaling_dynamic)} minutes in export event") + print(" PASS: Export rates increased by 100p/kWh for 2-hour period (14:00-16:00)") + print(f" PASS: Rate at 14:00 changed from {original_rate_before} to {base.rate_export[start_minutes]}") + print(f" PASS: Rate at 13:59 unchanged: {base.rate_export[start_minutes - 1]}") + print(f" PASS: Rate at 16:00 unchanged: {base.rate_export[end_minutes]}") + print(f" PASS: {len(rate_replicate)} minutes marked as 'saving' events") + print(f" PASS: load_scaling_saving ({base.load_scaling_saving}) applied to {len(base.load_scaling_dynamic)} minutes in export event") return False @@ -1171,12 +1171,12 @@ def get_arg(self, name, indirect=True): assert base.load_scaling_dynamic.get(start_minutes - 1) is None, "load_scaling_dynamic before event should be unchanged" assert base.load_scaling_dynamic.get(end_minutes) is None, "load_scaling_dynamic after event should be unchanged" - print(" ✓ Import rates decreased by 10p/kWh for 2-hour period (02:00-04:00)") - print(f" ✓ Rate at 02:00 changed from {original_rate} to {base.rate_import[start_minutes]}") - print(f" ✓ Rate at 01:59 unchanged: {base.rate_import[start_minutes - 1]}") - print(f" ✓ Rate at 04:00 unchanged: {base.rate_import[end_minutes]}") - print(f" ✓ {len(rate_replicate)} minutes marked as 'saving' events") - print(f" ✓ load_scaling_free ({base.load_scaling_free}) applied to {len(base.load_scaling_dynamic)} minutes in import event") + print(" PASS: Import rates decreased by 10p/kWh for 2-hour period (02:00-04:00)") + print(f" PASS: Rate at 02:00 changed from {original_rate} to {base.rate_import[start_minutes]}") + print(f" PASS: Rate at 01:59 unchanged: {base.rate_import[start_minutes - 1]}") + print(f" PASS: Rate at 04:00 unchanged: {base.rate_import[end_minutes]}") + print(f" PASS: {len(rate_replicate)} minutes marked as 'saving' events") + print(f" PASS: load_scaling_free ({base.load_scaling_free}) applied to {len(base.load_scaling_dynamic)} minutes in import event") return False @@ -1230,7 +1230,7 @@ def _test_axle_active_function(my_predbat=None): result = fetch_axle_active(test_interface_no_config) assert result is False, "fetch_axle_active should return False when axle_session is not configured" - print("✓ fetch_axle_active function test passed") + print("PASS: fetch_axle_active function test passed") return False @@ -1247,14 +1247,14 @@ def _test_axle_managed_initialization(my_predbat=None): assert axle.partner_password == "secret" assert axle.api_base_url == "https://api-sandbox.axle.energy" assert axle.api_key is None, "api_key should be None in managed mode" - print(" ✓ Valid managed mode initialisation") + print(" PASS: Valid managed mode initialisation") # Missing site_id should disable managed mode axle2 = MockAxleAPI() axle2.initialize(api_key=None, pence_per_kwh=100, automatic=False, managed_mode=True, site_id=None, partner_username="user@test.com", partner_password="secret") assert axle2.managed_mode is False, "managed_mode should be False when site_id missing" assert any("site_id" in msg for msg in axle2.log_messages), "Should log missing site_id" - print(" ✓ Missing site_id disables managed mode") + print(" PASS: Missing site_id disables managed mode") # Missing all managed params axle3 = MockAxleAPI() @@ -1262,7 +1262,7 @@ def _test_axle_managed_initialization(my_predbat=None): assert axle3.managed_mode is False, "managed_mode should be False when all params missing" assert any("partner_username" in msg for msg in axle3.log_messages) assert any("partner_password" in msg for msg in axle3.log_messages) - print(" ✓ Missing all params disables managed mode") + print(" PASS: Missing all params disables managed mode") return False @@ -1297,11 +1297,11 @@ def _test_axle_managed_price_curve_processing(my_predbat=None): assert len(export_events) == 2 assert len(import_events) == 2 - # First export: 50 GBP/MWh → 5.0 p/kWh + # First export: 50 GBP/MWh -> 5.0 p/kWh first_export = [e for e in export_events if "14:00:00" in e["start_time"]][0] assert first_export["pence_per_kwh"] == 5.0, f"Expected 5.0, got {first_export['pence_per_kwh']}" - # First import: negated → -5.0 p/kWh + # First import: negated -> -5.0 p/kWh first_import = [e for e in import_events if "14:00:00" in e["start_time"]][0] assert first_import["pence_per_kwh"] == -5.0, f"Expected -5.0, got {first_import['pence_per_kwh']}" @@ -1316,9 +1316,9 @@ def _test_axle_managed_price_curve_processing(my_predbat=None): null_events = [e for e in axle.event_history if "15:00:00" in e.get("start_time", "")] assert len(null_events) == 0, "Null price slots should be skipped" - print(" ✓ Price curve conversion correct (GBP/MWh → p/kWh)") - print(" ✓ Export and import sessions created per slot") - print(" ✓ Null prices skipped") + print(" PASS: Price curve conversion correct (GBP/MWh -> p/kWh)") + print(" PASS: Export and import sessions created per slot") + print(" PASS: Null prices skipped") return False @@ -1365,8 +1365,8 @@ def _test_axle_managed_event_dedup(my_predbat=None): export_event = [e for e in axle.event_history if e["import_export"] == "export"][0] assert export_event["pence_per_kwh"] == 6.0, "Deduped event should have updated price" - print(" ✓ Different directions at same time are kept separately") - print(" ✓ Same direction at same time is deduped with update") + print(" PASS: Different directions at same time are kept separately") + print(" PASS: Same direction at same time is deduped with update") return False @@ -1396,8 +1396,8 @@ def _test_axle_managed_future_events(my_predbat=None): axle.add_event_to_history(future_event, allow_future=True) assert len(axle.event_history) == 1, "Future event should be accepted with allow_future" - print(" ✓ Future events rejected without allow_future") - print(" ✓ Future events accepted with allow_future") + print(" PASS: Future events rejected without allow_future") + print(" PASS: Future events accepted with allow_future") return False @@ -1421,7 +1421,7 @@ def _test_axle_managed_publish(my_predbat=None): sensor = axle_managed.dashboard_items["binary_sensor.predbat_axle_event"] assert sensor["attributes"]["managed_mode"] is True, "Managed should have managed_mode=True" - print(" ✓ managed_mode attribute correctly set in sensor") + print(" PASS: managed_mode attribute correctly set in sensor") return False @@ -1450,8 +1450,8 @@ def _test_axle_managed_disabled_without_creds(my_predbat=None): axle2.initialize(api_key=None, pence_per_kwh=100, automatic=False, managed_mode=False) assert any("BYOK" in msg for msg in axle2.log_messages), "Should warn about BYOK needing api_key" - print(" ✓ Managed mode disabled when credentials missing") - print(" ✓ BYOK warning logged when managed_mode=False and no api_key") + print(" PASS: Managed mode disabled when credentials missing") + print(" PASS: BYOK warning logged when managed_mode=False and no api_key") return False @@ -1472,7 +1472,7 @@ def _test_axle_managed_get_partner_token(my_predbat=None): assert token == "tok_abc123", f"Expected tok_abc123, got {token}" assert axle.partner_token == "tok_abc123" assert axle.partner_token_expiry is not None - print(" ✓ Token fetched successfully") + print(" PASS: Token fetched successfully") # Test 2: Cached token returned without HTTP call call_count = [0] @@ -1489,7 +1489,7 @@ async def counting_aenter(*args): assert token2 == "tok_abc123", "Should return cached token" assert call_count[0] == 0, "Should not make HTTP call for cached token" - print(" ✓ Cached token returned without HTTP call") + print(" PASS: Cached token returned without HTTP call") # Test 3: Auth response missing access_token axle2 = MockAxleAPI() @@ -1504,7 +1504,7 @@ async def counting_aenter(*args): assert token3 is None, "Should return None when access_token missing from response" assert axle2.partner_token is None, "Should not cache empty token" assert any("missing access_token" in msg for msg in axle2.log_messages), "Should log missing token warning" - print(" ✓ Missing access_token handled correctly") + print(" PASS: Missing access_token handled correctly") # Test 4: Auth failure (401) axle3 = MockAxleAPI() @@ -1518,7 +1518,7 @@ async def counting_aenter(*args): assert token4 is None, "Should return None on auth failure" assert any("auth failed" in msg for msg in axle3.log_messages), "Should log auth failure" - print(" ✓ Auth failure returns None") + print(" PASS: Auth failure returns None") return False @@ -1585,9 +1585,9 @@ def session_factory(*args, **kwargs): assert axle.failures_total == 0 assert any("Price curve processed successfully" in msg for msg in axle.log_messages) - print(" ✓ Auth + price curve fetch works end-to-end") - print(" ✓ Sessions created with correct prices") - print(" ✓ Current event set to active export slot") + print(" PASS: Auth + price curve fetch works end-to-end") + print(" PASS: Sessions created with correct prices") + print(" PASS: Current event set to active export slot") return False @@ -1638,8 +1638,8 @@ def session_factory(*args, **kwargs): assert call_order[0] == 4, f"Expected 4 HTTP calls (auth, fail, re-auth, success), got {call_order[0]}" assert any("Price curve processed successfully" in msg for msg in axle.log_messages) - print(" ✓ Token invalidated after price curve failure") - print(" ✓ Re-auth and retry succeeds") + print(" PASS: Token invalidated after price curve failure") + print(" PASS: Re-auth and retry succeeds") # Test 2: Both attempts fail — should record failure axle2 = MockAxleAPI() @@ -1664,6 +1664,6 @@ def session_factory_all_fail(*args, **kwargs): assert axle2.failures_total == 1, f"Expected 1 failure, got {axle2.failures_total}" assert len(axle2.event_history) == 0, "No events should be created on complete failure" assert any("No price curve data after retry" in msg for msg in axle2.log_messages) - print(" ✓ Complete failure after retry records failure correctly") + print(" PASS: Complete failure after retry records failure correctly") return False diff --git a/apps/predbat/tests/test_balance_inverters.py b/apps/predbat/tests/test_balance_inverters.py index 05b9d1e93..f115a70d3 100644 --- a/apps/predbat/tests/test_balance_inverters.py +++ b/apps/predbat/tests/test_balance_inverters.py @@ -251,7 +251,7 @@ def test_balance_discharge_low_soc(my_predbat): print("ERROR: Expected inverter 0 discharge rate to be set to 0") return True - print("✓ Test passed: Low SoC inverter discharge rate set to 0") + print("PASS: Test passed: Low SoC inverter discharge rate set to 0") return False @@ -301,7 +301,7 @@ def test_balance_charge_high_soc(my_predbat): print("ERROR: Expected inverter 0 charge rate to be set to 0") return True - print("✓ Test passed: High SoC inverter charge rate set to 0") + print("PASS: Test passed: High SoC inverter charge rate set to 0") return False @@ -351,7 +351,7 @@ def test_balance_cross_charging1(my_predbat): print("ERROR: Expected inverter 2 charge rate to be set to 0 to stop cross-charging") return True - print("✓ Test passed: Cross-charging inverter charge rate set to 0") + print("PASS: Test passed: Cross-charging inverter charge rate set to 0") return False @@ -401,7 +401,7 @@ def test_balance_cross_charging2(my_predbat): print("ERROR: Expected inverter 1 discharge rate to be set to 0 to stop cross-charging") return True - print("✓ Test passed: Cross-charging inverter discharge rate set to 0") + print("PASS: Test passed: Cross-charging inverter discharge rate set to 0") return False @@ -451,7 +451,7 @@ def test_balance_cross_discharging(my_predbat): print("ERROR: Expected inverter 1 discharge rate to be set to 0 to stop cross-discharging") return True - print("✓ Test passed: Cross-discharging inverter discharge rate set to 0") + print("PASS: Test passed: Cross-discharging inverter discharge rate set to 0") return False @@ -498,7 +498,7 @@ def test_balance_already_balanced(my_predbat): print("ERROR: Expected no rate adjustments when inverters are balanced") return True - print("✓ Test passed: No adjustments made when already balanced") + print("PASS: Test passed: No adjustments made when already balanced") return False @@ -548,7 +548,7 @@ def test_balance_reset_balanced_charge(my_predbat): print("ERROR: Expected rate adjustments when inverters are balanced") return True - print("✓ Test passed: Rate adjustments made when already balanced") + print("PASS: Test passed: Rate adjustments made when already balanced") return False @@ -598,7 +598,7 @@ def test_balance_reset_balanced_discharge(my_predbat): print("ERROR: Expected rate adjustments when inverters are balanced") return True - print("✓ Test passed: Rate adjustments made when already balanced") + print("PASS: Test passed: Rate adjustments made when already balanced") return False @@ -644,7 +644,7 @@ def test_balance_below_threshold(my_predbat): print("ERROR: Expected no rate adjustments when difference below threshold") return True - print("✓ Test passed: No adjustments made when below threshold") + print("PASS: Test passed: No adjustments made when below threshold") return False @@ -692,7 +692,7 @@ def test_balance_at_reserve(my_predbat): print("ERROR: Expected inverter 0 at reserve to have discharge stopped") return True - print("✓ Test passed: Inverter at reserve has discharge stopped") + print("PASS: Test passed: Inverter at reserve has discharge stopped") return False @@ -738,7 +738,7 @@ def test_balance_disabled(my_predbat): print("ERROR: Expected no rate adjustments when balancing disabled") return True - print("✓ Test passed: No adjustments made when balancing disabled") + print("PASS: Test passed: No adjustments made when balancing disabled") return False @@ -791,7 +791,7 @@ def test_balance_calibration_mode(my_predbat): # Reset calibration mode my_predbat.inverters[0].in_calibration = False - print("✓ Test passed: No adjustments made when in calibration mode") + print("PASS: Test passed: No adjustments made when in calibration mode") return False @@ -840,5 +840,5 @@ def test_balance_insufficient_power(my_predbat): print("ERROR: Expected no rate adjustments when power below threshold") return True - print("✓ Test passed: No adjustments made when power insufficient") + print("PASS: Test passed: No adjustments made when power insufficient") return False diff --git a/apps/predbat/tests/test_battery_curve_keys.py b/apps/predbat/tests/test_battery_curve_keys.py index 512ce6b09..02f362e14 100644 --- a/apps/predbat/tests/test_battery_curve_keys.py +++ b/apps/predbat/tests/test_battery_curve_keys.py @@ -39,7 +39,7 @@ def test_get_curve_value_with_int_keys(my_predbat): assert get_curve_value(curve, 98) == 0.23 assert get_curve_value(curve, 97) == 0.3 assert get_curve_value(curve, 96) == 1.0 # default value - print("✓ Integer keys test passed") + print("PASS: Integer keys test passed") def test_get_curve_value_with_string_keys(my_predbat): @@ -52,7 +52,7 @@ def test_get_curve_value_with_string_keys(my_predbat): assert get_curve_value(curve, 98) == 0.23 assert get_curve_value(curve, 97) == 0.3 assert get_curve_value(curve, 96) == 1.0 # default value - print("✓ String keys test passed") + print("PASS: String keys test passed") def test_get_curve_value_with_mixed_keys(my_predbat): @@ -63,7 +63,7 @@ def test_get_curve_value_with_mixed_keys(my_predbat): assert get_curve_value(curve, 100) == 0.15 # int key found assert get_curve_value(curve, 99) == 0.20 # string key found - print("✓ Mixed keys test passed") + print("PASS: Mixed keys test passed") def test_get_curve_value_custom_default(my_predbat): @@ -74,7 +74,7 @@ def test_get_curve_value_custom_default(my_predbat): assert get_curve_value(curve, 100) == 0.15 assert get_curve_value(curve, 99, default=0.5) == 0.5 assert get_curve_value(curve, 98, default=0.0) == 0.0 - print("✓ Custom default test passed") + print("PASS: Custom default test passed") def test_validate_config_auto_curve(my_predbat): @@ -91,7 +91,7 @@ def test_validate_config_auto_curve(my_predbat): my_predbat.args["battery_discharge_power_curve"] = "auto" errors_with_auto = my_predbat.validate_config() assert errors_with_auto == baseline_errors, f"Setting curves to 'auto' introduced extra validation errors: baseline={baseline_errors}, with_auto={errors_with_auto}" - print("✓ Auto curve validation test passed") + print("PASS: Auto curve validation test passed") finally: my_predbat.args = original_args diff --git a/apps/predbat/tests/test_calculate_yesterday.py b/apps/predbat/tests/test_calculate_yesterday.py index ac74a0da5..179910aa2 100644 --- a/apps/predbat/tests/test_calculate_yesterday.py +++ b/apps/predbat/tests/test_calculate_yesterday.py @@ -345,7 +345,7 @@ def _test_car_slot_subtraction(my_predbat, failed): in_car_slot returns 1.2 kW = 0.1 kWh per 5-min step). The step_data_history mock returns FLAT_LOAD_KWH (0.1 kWh) for every step. - After the fix (kW → kWh/step conversion): + After the fix (kW -> kWh/step conversion): - Inside-slot steps: max(0.1 - 1.2 * 5/60, 0) = max(0.0, 0) = 0.0 - Outside-slot steps: unchanged at FLAT_LOAD_KWH @@ -360,7 +360,7 @@ def _test_car_slot_subtraction(my_predbat, failed): # calculate_yesterday will NOT re-run load_octopus_slots; it will use # whatever we put in car_charging_slots directly. my_predbat.num_cars = 1 - # Slot: minute 60..180, 2.4 kWh total over 2 hours → 1.2 kW = 0.1 kWh/step + # Slot: minute 60..180, 2.4 kWh total over 2 hours -> 1.2 kW = 0.1 kWh/step car_slot = {"start": 60, "end": 180, "kwh": 2.4, "average": 20} my_predbat.car_charging_slots[0] = [car_slot] car_charging_slots_before = copy.deepcopy(my_predbat.car_charging_slots) @@ -371,7 +371,7 @@ def _test_car_slot_subtraction(my_predbat, failed): my_predbat.car_charging_limit = [80.0, 100.0, 100.0, 100.0] car_charging_soc_before = list(my_predbat.car_charging_soc) - # octopus_intelligent_slot not configured → entity_id_list is empty + # octopus_intelligent_slot not configured -> entity_id_list is empty # so the re-load loop in calculate_yesterday is skipped my_predbat.args["octopus_intelligent_slot"] = None @@ -445,7 +445,7 @@ def _test_car_slot_from_energy_sensor(my_predbat, failed): synthesise a slot for that window and subtract the corresponding kWh from the load step data before handing it to the Prediction. - Slot: start=60, end=90, kwh=1.5 → load rate = 1.5/0.5h = 3.0 kW + Slot: start=60, end=90, kwh=1.5 -> load rate = 1.5/0.5h = 3.0 kW Subtraction per 5-min step: 3.0 * 5/60 = 0.25 kWh > FLAT_LOAD_KWH (0.1) Expected inside-slot value: max(0.1 - 0.25, 0) = 0.0 @@ -466,7 +466,7 @@ def _test_car_slot_from_energy_sensor(my_predbat, failed): my_predbat.car_charging_limit = [80.0, 100.0, 100.0, 100.0] car_charging_soc_before = list(my_predbat.car_charging_soc) - # No octopus slot configured → entity_id_list is empty, octopus path skipped. + # No octopus slot configured -> entity_id_list is empty, octopus path skipped. my_predbat.args["octopus_intelligent_slot"] = None my_predbat.octopus_slots = [[], [], [], []] my_predbat.octopus_intelligent_consider_full = False @@ -621,7 +621,7 @@ def _test_reconstruct_car_slots(my_predbat, failed): failed = True # With car_energy_reported_load=True, the slot kwh is capped to load_reported # * car_charging_loss. load_reported = plan_iv/PREDICT_STEP * FLAT_LOAD_KWH - # = 6 * 0.1 = 0.6 kWh; car_charging_loss=1.0 → expected kwh = 0.6. + # = 6 * 0.1 = 0.6 kWh; car_charging_loss=1.0 -> expected kwh = 0.6. load_steps_in_slot = plan_iv // PREDICT_STEP expected_slot_kwh = load_steps_in_slot * FLAT_LOAD_KWH * my_predbat.car_charging_loss if abs(slot.get("kwh", -1) - expected_slot_kwh) > 1e-9: @@ -632,7 +632,7 @@ def _test_reconstruct_car_slots(my_predbat, failed): failed = True # Steps inside the slot [60, 90) should be zeroed out. - # slot kwh adjusted to 0.6 → rate = 0.6/0.5h = 1.2 kW; 1.2*5/60 = 0.1 kWh/step = FLAT_LOAD_KWH → max(0.1-0.1,0)=0.0 + # slot kwh adjusted to 0.6 -> rate = 0.6/0.5h = 1.2 kW; 1.2*5/60 = 0.1 kWh/step = FLAT_LOAD_KWH -> max(0.1-0.1,0)=0.0 slot_end = 60 + my_predbat.plan_interval_minutes for inside_min in range(60, slot_end, PREDICT_STEP): val = yesterday_load_step.get(inside_min, -1) @@ -739,9 +739,9 @@ def _mock_load_octopus_slots(car_n, raw_slots, consider_full): my_predbat.car_charging_energy = {} plan_iv = my_predbat.plan_interval_minutes # 30 - # A slot claiming 3.0 kWh → kwh_drain = 3.0 kWh; needs load ≥ 0.3 kWh to survive. - # Provide only 0.01 kWh per step → load_reported = 6 * 0.01 = 0.06 kWh. - # 0.06 * 10 = 0.6 < 3.0 → slot should be cancelled (kwh set to 0). + # A slot claiming 3.0 kWh -> kwh_drain = 3.0 kWh; needs load ≥ 0.3 kWh to survive. + # Provide only 0.01 kWh per step -> load_reported = 6 * 0.01 = 0.06 kWh. + # 0.06 * 10 = 0.6 < 3.0 -> slot should be cancelled (kwh set to 0). TINY_LOAD = 0.01 cancelled_slot = {"start": 60, "end": 60 + plan_iv, "kwh": 3.0, "octopus": True} my_predbat.car_charging_slots = [[cancelled_slot], [], [], []] @@ -754,7 +754,7 @@ def _mock_load_octopus_slots(car_n, raw_slots, consider_full): print("ERROR 5d: slot kwh should be 0 after cancellation, got {}".format(cancelled_slot.get("kwh"))) failed = True - # Because kwh=0, subtract_amount=0 → load values should be completely unchanged. + # Because kwh=0, subtract_amount=0 -> load values should be completely unchanged. for m in range(60, 60 + plan_iv, PREDICT_STEP): val = yesterday_load_step_5d.get(m, -1) if abs(val - TINY_LOAD) > 1e-9: @@ -776,8 +776,8 @@ def _mock_load_octopus_slots(car_n, raw_slots, consider_full): my_predbat.car_charging_energy = {} plan_iv = my_predbat.plan_interval_minutes # 30 - # Slot claims 2.4 kWh → kwh_drain = 2.4; load = 6 * 0.2 = 1.2 kWh. - # 1.2 * 10 = 12.0 ≥ 2.4 → adjusted: slot["kwh"] = 1.2 * 1.0 = 1.2 kWh. + # Slot claims 2.4 kWh -> kwh_drain = 2.4; load = 6 * 0.2 = 1.2 kWh. + # 1.2 * 10 = 12.0 ≥ 2.4 -> adjusted: slot["kwh"] = 1.2 * 1.0 = 1.2 kWh. # Subtraction: 1.2 kWh / 0.5 h = 2.4 kW; 2.4 * 5/60 = 0.2 kWh/step. # After subtraction: max(0.2 - 0.2, 0) = 0.0 inside slot. MEDIUM_LOAD = 0.2 @@ -820,13 +820,13 @@ def _mock_load_octopus_slots(car_n, raw_slots, consider_full): my_predbat.args["octopus_intelligent_slot"] = None my_predbat.car_charging_energy = {} - plan_iv = my_predbat.plan_interval_minutes # 30 minutes → 6 steps + plan_iv = my_predbat.plan_interval_minutes # 30 minutes -> 6 steps # Load: 0.2 kWh/step × 6 steps = 1.2 kWh total in the window. - # Car 0: kwh=0.6, kwh_drain=0.6 → load 1.2 ≥ 0.6 → no scaling. - # subtract: 0.6/0.5h × 5/60 = 0.1 kWh/step → residual = 0.1 kWh/step. - # Car 1: kwh=0.9, kwh_drain=0.9 → load 6×0.1=0.6 < 0.9. - # 0.6×10=6.0 ≥ 0.9 → adjusted to 0.6 kWh. - # subtract: 0.6/0.5h × 5/60 = 0.1 kWh/step → residual = 0.0. + # Car 0: kwh=0.6, kwh_drain=0.6 -> load 1.2 ≥ 0.6 -> no scaling. + # subtract: 0.6/0.5h × 5/60 = 0.1 kWh/step -> residual = 0.1 kWh/step. + # Car 1: kwh=0.9, kwh_drain=0.9 -> load 6×0.1=0.6 < 0.9. + # 0.6×10=6.0 ≥ 0.9 -> adjusted to 0.6 kWh. + # subtract: 0.6/0.5h × 5/60 = 0.1 kWh/step -> residual = 0.0. TWO_CAR_LOAD = 0.2 slot_car0 = {"start": 60, "end": 60 + plan_iv, "kwh": 0.6, "octopus": True} slot_car1 = {"start": 60, "end": 60 + plan_iv, "kwh": 0.9, "octopus": True} @@ -846,7 +846,7 @@ def _mock_load_octopus_slots(car_n, raw_slots, consider_full): print("ERROR 5f: car 1 slot kwh should be {} after scaling, got {}".format(expected_car1_kwh, slot_car1.get("kwh"))) failed = True - # Both cars have subtracted their full amounts → inside-slot load should be 0. + # Both cars have subtracted their full amounts -> inside-slot load should be 0. for m in range(60, 60 + plan_iv, PREDICT_STEP): val = yesterday_load_step_5f.get(m, -1) if abs(val) > 1e-9: @@ -886,7 +886,7 @@ def _test_soc_not_mutated_and_override_passed(my_predbat, failed): # soc_yesterday is read from the HA entity prefix+".savings_total_soc". # No entity is registered in the test dummy store, so get_state_wrapper - # returns the default 0.0 → soc_yesterday == 0.0. + # returns the default 0.0 -> soc_yesterday == 0.0. expected_soc_yesterday = 0.0 # Capture (base_soc_kw, prediction_soc_kw, prediction_soc_max) for each diff --git a/apps/predbat/tests/test_carbon.py b/apps/predbat/tests/test_carbon.py index b93610011..c383fb126 100644 --- a/apps/predbat/tests/test_carbon.py +++ b/apps/predbat/tests/test_carbon.py @@ -123,13 +123,13 @@ def test_carbon(my_predbat=None): try: test_result = test_func(my_predbat) if test_result: - print(f"✗ FAILED: {test_name}") + print(f"FAIL: FAILED: {test_name}") failed += 1 else: - print(f"✓ PASSED: {test_name}") + print(f"PASS: PASSED: {test_name}") passed += 1 except Exception as e: - print(f"✗ EXCEPTION in {test_name}: {e}") + print(f"FAIL: EXCEPTION in {test_name}: {e}") import traceback traceback.print_exc() @@ -156,23 +156,23 @@ def _test_carbon_initialization(my_predbat=None): api.automatic = True if api.postcode != "SW1A 1AA": - print(" ✗ ERROR: Postcode not set correctly") + print(" FAIL: ERROR: Postcode not set correctly") failed = 1 if api.automatic != True: - print(" ✗ ERROR: Automatic flag not set correctly") + print(" FAIL: ERROR: Automatic flag not set correctly") failed = 1 if api.failures_total != 0: - print(" ✗ ERROR: failures_total should be 0") + print(" FAIL: ERROR: failures_total should be 0") failed = 1 if api.carbon_data_points != []: - print(" ✗ ERROR: carbon_data_points should be empty list") + print(" FAIL: ERROR: carbon_data_points should be empty list") failed = 1 if not failed: - print(" ✓ CarbonAPI initialised correctly") + print(" PASS: CarbonAPI initialised correctly") return failed @@ -211,7 +211,7 @@ def _test_fetch_carbon_data_success(my_predbat=None): print(f"ERROR: Expected intensity 265, got {first_point['intensity']}") return 1 - print(" ✓ Carbon data fetched successfully") + print(" PASS: Carbon data fetched successfully") return failed @@ -240,7 +240,7 @@ def _test_fetch_carbon_data_http_error(my_predbat=None): print(f"ERROR: Should not have collected data on HTTP error") return 1 - print(" ✓ HTTP error handled correctly") + print(" PASS: HTTP error handled correctly") return 0 @@ -267,7 +267,7 @@ def _test_fetch_carbon_data_timeout(my_predbat=None): print(f"ERROR: Should not have collected data on timeout") return 1 - print(" ✓ Timeout handled correctly") + print(" PASS: Timeout handled correctly") return 0 @@ -296,7 +296,7 @@ def _test_fetch_carbon_data_json_error(my_predbat=None): print(f"ERROR: Should not have collected data on JSON error") return 1 - print(" ✓ JSON error handled correctly") + print(" PASS: JSON error handled correctly") return 0 @@ -326,7 +326,7 @@ def _test_fetch_carbon_data_empty(my_predbat=None): print("ERROR: Should log warning about empty data") return 1 - print(" ✓ Empty data handled correctly") + print(" PASS: Empty data handled correctly") return 0 @@ -348,7 +348,7 @@ def _test_fetch_carbon_data_cache_skip(my_predbat=None): print(f"ERROR: Expected 0 API calls (cache), got {mock_session_class.call_count}") return 1 - print(" ✓ Cache skip working correctly") + print(" PASS: Cache skip working correctly") return 0 @@ -379,7 +379,7 @@ def _test_fetch_carbon_data_cache_refresh(my_predbat=None): print("ERROR: Should have collected data after cache refresh") return 1 - print(" ✓ Cache refresh working correctly") + print(" PASS: Cache refresh working correctly") return 0 @@ -434,7 +434,7 @@ def _test_publish_carbon_data_current(my_predbat=None): print("ERROR: Forecast attribute doesn't match data points") return 1 - print(" ✓ Current intensity published correctly") + print(" PASS: Current intensity published correctly") return 0 @@ -484,7 +484,7 @@ def _test_publish_carbon_data_forecast(my_predbat=None): print(f"ERROR: Expected intensity=265, got {first_item['intensity']}") return 1 - print(" ✓ Forecast attribute published correctly") + print(" PASS: Forecast attribute published correctly") return 0 @@ -514,7 +514,7 @@ def _test_publish_carbon_data_unknown(my_predbat=None): print(f"ERROR: Expected state 'unknown', got '{item['state']}'") return 1 - print(" ✓ Unknown state handled correctly") + print(" PASS: Unknown state handled correctly") return 0 @@ -548,7 +548,7 @@ def _test_postcode_stripping(my_predbat=None): print("ERROR: Stripped postcode 'BS16' not found in URL") return 1 - print(" ✓ Postcode stripping working correctly") + print(" PASS: Postcode stripping working correctly") return 0 @@ -591,7 +591,7 @@ def _test_multiple_date_fetches(my_predbat=None): print(f"ERROR: Second call should contain date 2025-12-22") return 1 - print(" ✓ Multiple date fetches working correctly") + print(" PASS: Multiple date fetches working correctly") return 0 @@ -626,7 +626,7 @@ def _test_time_format_conversion(my_predbat=None): print(f"ERROR: Expected to to start with '2025-12-20T14:30:00', got '{point['to']}'") return 1 - print(" ✓ Time format conversion working correctly") + print(" PASS: Time format conversion working correctly") return 0 @@ -668,7 +668,7 @@ def _test_timezone_handling(my_predbat=None): print(f"ERROR: Failed to parse stored time: {e}") return 1 - print(" ✓ Timezone handling working correctly") + print(" PASS: Timezone handling working correctly") return 0 @@ -733,7 +733,7 @@ async def session_aexit(*args): print(f"ERROR: Missing expected intensities. Got: {intensities}") return 1 - print(" ✓ Data collection from multiple dates working correctly") + print(" PASS: Data collection from multiple dates working correctly") return 0 @@ -769,7 +769,7 @@ def _test_failure_counter(my_predbat=None): print(f"ERROR: Expected failures_total=4 after timeout, got {failures_after_timeout}") return 1 - print(" ✓ Failure counter working correctly") + print(" PASS: Failure counter working correctly") return 0 @@ -802,7 +802,7 @@ def _test_run_first_call(my_predbat=None): print(f"ERROR: Expected 2 API calls on first run, got {mock_session.get.call_count}") return 1 - print(" ✓ First run calling fetch correctly") + print(" PASS: First run calling fetch correctly") return 0 @@ -834,7 +834,7 @@ def _test_run_15min_interval(my_predbat=None): print(f"ERROR: Expected 0 API calls at non-15-min interval, got {mock_session.get.call_count}") return 1 - print(" ✓ 15-minute interval working correctly") + print(" PASS: 15-minute interval working correctly") return 0 @@ -878,5 +878,5 @@ def _test_automatic_config_flow(my_predbat=None): print("ERROR: carbon_intensity config should not be set when automatic=False") return 1 - print(" ✓ Automatic config flow working correctly") + print(" PASS: Automatic config flow working correctly") return 0 diff --git a/apps/predbat/tests/test_compare.py b/apps/predbat/tests/test_compare.py index 574fa588a..4b075c0c2 100644 --- a/apps/predbat/tests/test_compare.py +++ b/apps/predbat/tests/test_compare.py @@ -296,7 +296,7 @@ def _mock_fetch_config_options(): # Do NOT restore – replicate the old buggy code # Now run a second tariff with no config – value should still be 2.0 (bleed) - cmp.fetch_config({}) # empty config → no changes + cmp.fetch_config({}) # empty config -> no changes if pb.config_index["best_soc_min"]["value"] == 0.5: print("ERROR T10: expected to detect config bleed when restore is skipped") diff --git a/apps/predbat/tests/test_component_base.py b/apps/predbat/tests/test_component_base.py index db825eb45..8caf4754c 100644 --- a/apps/predbat/tests/test_component_base.py +++ b/apps/predbat/tests/test_component_base.py @@ -92,8 +92,8 @@ async def run_test(): # Start component in background task = asyncio.create_task(component.start()) - # Wait briefly for it to start (1.0 → 0.01s real via fast_sleep; must be - # shorter than the component's 5s loop sleep → 0.05s real) + # Wait briefly for it to start (1.0 -> 0.01s real via fast_sleep; must be + # shorter than the component's 5s loop sleep -> 0.05s real) await asyncio.sleep(1.0) # Check it started successfully @@ -128,7 +128,7 @@ async def run_test(): assert not component.api_started, "Component should not have started yet (failed first attempt)" # Wait slightly longer - should still be 1 run (waiting for backoff) - # Backoff = 60s real → 0.6s real via fast_sleep; we wait 0.5 → 0.005s real + # Backoff = 60s real -> 0.6s real via fast_sleep; we wait 0.5 -> 0.005s real await asyncio.sleep(0.5) assert component.run_count == 1, f"Should still be 1 run (waiting for backoff), got {component.run_count}" @@ -189,7 +189,7 @@ async def run_test(): initial_run_count = component.run_count # Wait a bit more - should not run again immediately (only every 60 seconds) - # Component loop sleep = 5s → 0.05s real; we wait 0.5 → 0.005s real + # Component loop sleep = 5s -> 0.05s real; we wait 0.5 -> 0.005s real await asyncio.sleep(0.5) assert component.run_count == initial_run_count, f"Should not run again immediately, expected {initial_run_count}, got {component.run_count}" @@ -333,7 +333,7 @@ async def run_test(): task = asyncio.create_task(component.start()) - # Wait long enough (sped up 100x by fast_sleep → ~2s real) for the component + # Wait long enough (sped up 100x by fast_sleep -> ~2s real) for the component # loop to advance past simulated seconds=60 so a steady-state run can occur. await asyncio.sleep(200) diff --git a/apps/predbat/tests/test_db_manager.py b/apps/predbat/tests/test_db_manager.py index fe56ae7c6..a858991c9 100644 --- a/apps/predbat/tests/test_db_manager.py +++ b/apps/predbat/tests/test_db_manager.py @@ -104,13 +104,13 @@ def test_db_manager(my_predbat=None): try: result = test_func(my_predbat) if result: - print(f"✗ FAILED: {key}") + print(f"FAIL: FAILED: {key}") failed += 1 else: - print(f"✓ PASSED: {key}") + print(f"PASS: PASSED: {key}") passed += 1 except Exception as e: - print(f"✗ EXCEPTION in {key}: {e}") + print(f"FAIL: EXCEPTION in {key}: {e}") import traceback traceback.print_exc() @@ -149,7 +149,7 @@ async def run_test(): timeout += 1 assert db_mgr.api_started, "DatabaseManager failed to start" - print("✓ DatabaseManager started successfully") + print("PASS: DatabaseManager started successfully") # Test 1: Set state with attributes entity_id = "sensor.test_battery" @@ -164,7 +164,7 @@ async def run_test(): assert "last_changed" in result, "set_state_db should return last_changed" assert result["state"] == state, "Returned state doesn't match" assert result["attributes"] == attributes, "Returned attributes don't match" - print(f"✓ set_state_db returned correct structure: {result}") + print(f"PASS: set_state_db returned correct structure: {result}") # Test 2: Get state back and verify retrieved = await loop.run_in_executor(None, db_mgr.get_state_db, entity_id) @@ -174,7 +174,7 @@ async def run_test(): assert retrieved["attributes"]["unit_of_measurement"] == "kWh", "Attribute unit_of_measurement mismatch" assert retrieved["attributes"]["friendly_name"] == "Test Battery", "Attribute friendly_name mismatch" assert retrieved["attributes"]["device_class"] == "energy", "Attribute device_class mismatch" - print(f"✓ get_state_db retrieved correct state and attributes: {retrieved}") + print(f"PASS: get_state_db retrieved correct state and attributes: {retrieved}") # Test 3: Set state with custom timestamp (test timezone conversion) # GMT+2 10:00 should be stored as GMT+0 08:00 in the database @@ -198,14 +198,14 @@ async def run_test(): expected_gmt0_time = datetime(2025, 12, 25, 8, 0, 0) # GMT+2 10:00 -> GMT+0 08:00 assert parsed_ts.hour == expected_gmt0_time.hour, f"Timezone conversion failed: expected hour {expected_gmt0_time.hour} (GMT+0), got {parsed_ts.hour}" assert parsed_ts.day == expected_gmt0_time.day, f"Timezone conversion failed: expected day {expected_gmt0_time.day}, got {parsed_ts.day}" - print(f"✓ Timezone conversion verified: GMT+2 10:00 -> GMT+0 {parsed_ts.strftime('%H:%M:%S')}") + print(f"PASS: Timezone conversion verified: GMT+2 10:00 -> GMT+0 {parsed_ts.strftime('%H:%M:%S')}") # Test 4: Update existing entity with new state state3 = "50.0" await loop.run_in_executor(None, db_mgr.set_state_db, entity_id, state3, attributes) retrieved3 = await loop.run_in_executor(None, db_mgr.get_state_db, entity_id) assert retrieved3["state"] == state3, f"Expected updated state {state3}, got {retrieved3['state']}" - print(f"✓ State update works correctly: {retrieved3['state']}") + print(f"PASS: State update works correctly: {retrieved3['state']}") # Stop the component and verify thread exits await db_mgr.stop() @@ -217,15 +217,15 @@ async def run_test(): timeout += 1 assert not db_mgr.api_started, "DatabaseManager thread did not exit after stop command" - print("✓ DatabaseManager thread exited after stop command") + print("PASS: DatabaseManager thread exited after stop command") await task - print("✓ DatabaseManager stopped successfully") + print("PASS: DatabaseManager stopped successfully") finally: # Cleanup temp directory shutil.rmtree(mock_base.config_root) - print(f"✓ Cleaned up temp directory: {mock_base.config_root}") + print(f"PASS: Cleaned up temp directory: {mock_base.config_root}") run_async(run_test()) print("=== test_db_manager_set_get_state PASSED ===\n") @@ -256,7 +256,7 @@ async def run_test(): timeout += 1 assert db_mgr.api_started, "DatabaseManager failed to start" - print("✓ DatabaseManager started successfully") + print("PASS: DatabaseManager started successfully") # Test 1: Create multiple entities entities_to_create = [ @@ -279,7 +279,7 @@ async def run_test(): for entity_id, _, _ in entities_to_create: assert entity_id in all_entities, f"Entity {entity_id} not in returned list" - print(f"✓ get_all_entities_db returned correct entities: {all_entities}") + print(f"PASS: get_all_entities_db returned correct entities: {all_entities}") # Test 3: Create historical data for one entity history_entity = "sensor.battery_soc" @@ -308,7 +308,7 @@ async def run_test(): else: history_data = history - print(f"✓ get_history_db returned {len(history_data)} history entries") + print(f"PASS: get_history_db returned {len(history_data)} history entries") # Verify history entries have correct structure for entry in history_data[:3]: # Check first 3 entries @@ -320,7 +320,7 @@ async def run_test(): history_states = [entry.get("state") for entry in history_data] found_count = sum(1 for hs in historical_states if hs["state"] in history_states) assert found_count > 0, f"None of the historical states found in returned history" - print(f"✓ Found {found_count} of {len(historical_states)} expected historical states") + print(f"PASS: Found {found_count} of {len(historical_states)} expected historical states") # Test 5: Verify timestamp format for entry in history_data[:3]: @@ -331,7 +331,7 @@ async def run_test(): # Remove 'Z' suffix if present ts = timestamp_str.rstrip("Z") parsed = datetime.strptime(ts, TIME_FORMAT_DB) - print(f"✓ Timestamp format correct: {timestamp_str}") + print(f"PASS: Timestamp format correct: {timestamp_str}") break except ValueError as e: print(f"Warning: Timestamp format issue: {timestamp_str} - {e}") @@ -346,16 +346,16 @@ async def run_test(): timeout += 1 assert not db_mgr.api_started, "DatabaseManager thread did not exit after stop command" - print("✓ DatabaseManager thread exited after stop command") + print("PASS: DatabaseManager thread exited after stop command") await task - print("✓ DatabaseManager stopped successfully") + print("PASS: DatabaseManager stopped successfully") finally: # Cleanup shutil.rmtree(mock_base.config_root) - print(f"✓ Cleaned up temp directory: {mock_base.config_root}") + print(f"PASS: Cleaned up temp directory: {mock_base.config_root}") run_async(run_test()) print("=== test_db_manager_entities_and_history PASSED ===\n") @@ -386,14 +386,14 @@ async def run_test(): timeout += 1 assert db_mgr.api_started, "DatabaseManager failed to start" - print("✓ DatabaseManager started successfully") + print("PASS: DatabaseManager started successfully") # Test 1: Get state for non-existent entity loop = asyncio.get_event_loop() non_existent = "sensor.does_not_exist" result = await loop.run_in_executor(None, db_mgr.get_state_db, non_existent) assert result is None, f"Expected None for non-existent entity, got {result}" - print(f"✓ get_state_db correctly returned None for non-existent entity") + print(f"PASS: get_state_db correctly returned None for non-existent entity") # Test 2: Get history for non-existent entity (run in executor) now = datetime(2025, 12, 25, 12, 0, 0, tzinfo=mock_base.local_tz) @@ -401,7 +401,7 @@ async def run_test(): # History should return empty list or [[]] if history: assert len(history) == 0 or (len(history) == 1 and len(history[0]) == 0), f"Expected empty history for non-existent entity, got {history}" - print(f"✓ get_history_db correctly returned empty for non-existent entity") + print(f"PASS: get_history_db correctly returned empty for non-existent entity") # Test 3: Verify IPC queue is still working after errors # Set a valid entity to confirm queue isn't broken @@ -413,13 +413,13 @@ async def run_test(): retrieved = await loop.run_in_executor(None, db_mgr.get_state_db, entity_id) assert retrieved is not None, "IPC queue broken after error handling" assert retrieved["state"] == state, "State mismatch after error handling" - print(f"✓ IPC queue still functional after error handling") + print(f"PASS: IPC queue still functional after error handling") # Test 4: Get all entities should work even if empty initially all_entities = await loop.run_in_executor(None, db_mgr.get_all_entities_db) assert isinstance(all_entities, list), "get_all_entities_db should always return a list" assert entity_id in all_entities, "Entity should be in entities list" - print(f"✓ get_all_entities_db returned valid list: {len(all_entities)} entities") + print(f"PASS: get_all_entities_db returned valid list: {len(all_entities)} entities") # Stop the component and verify thread exits # Send stop command via IPC to cover the queue processing path @@ -433,16 +433,16 @@ async def run_test(): timeout += 1 assert not db_mgr.api_started, "DatabaseManager thread did not exit after stop command" - print("✓ DatabaseManager thread exited after stop command") + print("PASS: DatabaseManager thread exited after stop command") await task - print("✓ DatabaseManager stopped successfully") + print("PASS: DatabaseManager stopped successfully") finally: # Cleanup shutil.rmtree(mock_base.config_root) - print(f"✓ Cleaned up temp directory: {mock_base.config_root}") + print(f"PASS: Cleaned up temp directory: {mock_base.config_root}") run_async(run_test()) print("=== test_db_manager_error_handling PASSED ===\n") @@ -473,7 +473,7 @@ async def run_test(): timeout += 1 assert db_mgr1.api_started, "DatabaseManager failed to start" - print("✓ DatabaseManager started successfully (first instance)") + print("PASS: DatabaseManager started successfully (first instance)") # Store test data loop = asyncio.get_event_loop() @@ -489,7 +489,7 @@ async def run_test(): # Verify data is stored all_entities = await loop.run_in_executor(None, db_mgr1.get_all_entities_db) assert len(all_entities) >= 3, f"Expected at least 3 entities before restart, got {len(all_entities)}" - print(f"✓ Stored {len(all_entities)} entities: {all_entities}") + print(f"PASS: Stored {len(all_entities)} entities: {all_entities}") # Stop the first instance and verify thread exits await db_mgr1.stop() @@ -503,7 +503,7 @@ async def run_test(): assert not db_mgr1.api_started, "DatabaseManager thread did not exit after stop command" await task1 - print("✓ DatabaseManager stopped and thread exited (first instance)") + print("PASS: DatabaseManager stopped and thread exited (first instance)") # Wait a bit to ensure everything is closed await asyncio.sleep(0.1) @@ -525,7 +525,7 @@ async def run_test(): timeout += 1 assert db_mgr2.api_started, "DatabaseManager failed to start (second instance)" - print("✓ DatabaseManager restarted successfully (second instance)") + print("PASS: DatabaseManager restarted successfully (second instance)") # Verify all entities are still present all_entities_after = await loop.run_in_executor(None, db_mgr2.get_all_entities_db) @@ -534,7 +534,7 @@ async def run_test(): for entity_id, _, _ in test_data: assert entity_id in all_entities_after, f"Entity {entity_id} not found after restart" - print(f"✓ All {len(all_entities_after)} entities persisted after restart") + print(f"PASS: All {len(all_entities_after)} entities persisted after restart") # Verify state and attributes are preserved for entity_id, expected_state, expected_attributes in test_data: @@ -547,7 +547,7 @@ async def run_test(): assert attr_key in retrieved["attributes"], f"Attribute {attr_key} missing for {entity_id} after restart" assert retrieved["attributes"][attr_key] == attr_value, f"Attribute {attr_key} mismatch for {entity_id}: expected {attr_value}, got {retrieved['attributes'][attr_key]}" - print(f"✓ Entity {entity_id}: state={retrieved['state']}, attributes preserved") + print(f"PASS: Entity {entity_id}: state={retrieved['state']}, attributes preserved") # Stop the second instance and verify thread exits await db_mgr2.stop() @@ -560,12 +560,12 @@ async def run_test(): assert not db_mgr2.api_started, "DatabaseManager thread did not exit after stop command" await task2 - print("✓ DatabaseManager stopped and thread exited (second instance)") + print("PASS: DatabaseManager stopped and thread exited (second instance)") finally: # Cleanup shutil.rmtree(mock_base.config_root) - print(f"✓ Cleaned up temp directory: {mock_base.config_root}") + print(f"PASS: Cleaned up temp directory: {mock_base.config_root}") run_async(run_test()) print("=== test_db_manager_persistence PASSED ===\n") @@ -596,7 +596,7 @@ async def run_test(): timeout += 1 assert db_mgr.api_started, "DatabaseManager failed to start" - print("✓ DatabaseManager started successfully") + print("PASS: DatabaseManager started successfully") loop = asyncio.get_event_loop() @@ -609,7 +609,7 @@ async def run_test(): first_commit_time = db_mgr.last_commit_time assert first_commit_time is not None, "First commit should have happened (last_commit_time should be set)" - print(f"✓ First commit happened at {first_commit_time}") + print(f"PASS: First commit happened at {first_commit_time}") # Test 2: Rapid writes within 5 seconds should NOT trigger additional commits await asyncio.sleep(0.1) @@ -622,7 +622,7 @@ async def run_test(): second_check_time = db_mgr.last_commit_time assert second_check_time == first_commit_time, "No commit should happen within 5 seconds of last commit" - print(f"✓ Rapid writes within 5 seconds did not trigger new commits (last_commit_time unchanged)") + print(f"PASS: Rapid writes within 5 seconds did not trigger new commits (last_commit_time unchanged)") # Test 3: Push back commit time to 6 seconds ago, then write - should trigger a new commit print(" Pushing last_commit_time back to 6 seconds ago to test throttle expiry...") @@ -638,7 +638,7 @@ async def run_test(): assert third_commit_time > first_commit_time, "New commit time should be later than first commit" time_diff = (third_commit_time - pushed_back_time).total_seconds() assert time_diff >= 5.0, f"Time difference should show commit happened after 5+ second gap, got {time_diff}" - print(f"✓ After pushing time back 6s, new commit triggered at {third_commit_time}") + print(f"PASS: After pushing time back 6s, new commit triggered at {third_commit_time}") # Test 4: Verify all data was persisted despite throttling retrieved1 = await loop.run_in_executor(None, db_mgr.get_state_db, "sensor.test1") @@ -652,7 +652,7 @@ async def run_test(): assert retrieved3 is not None and retrieved3["state"] == "30", "sensor.test3 data should be persisted" assert retrieved4 is not None and retrieved4["state"] == "40", "sensor.test4 data should be persisted" assert retrieved5 is not None and retrieved5["state"] == "50", "sensor.test5 data should be persisted" - print("✓ All data persisted correctly despite commit throttling") + print("PASS: All data persisted correctly despite commit throttling") # Stop and cleanup await db_mgr.stop() @@ -662,12 +662,12 @@ async def run_test(): timeout += 1 await task - print("✓ DatabaseManager stopped successfully") + print("PASS: DatabaseManager stopped successfully") finally: # Cleanup temp directory shutil.rmtree(mock_base.config_root) - print(f"✓ Cleaned up temp directory: {mock_base.config_root}") + print(f"PASS: Cleaned up temp directory: {mock_base.config_root}") run_async(run_test()) print("=== test_db_manager_commit_throttling PASSED ===\n") diff --git a/apps/predbat/tests/test_download.py b/apps/predbat/tests/test_download.py index fe05a2730..01b97c912 100644 --- a/apps/predbat/tests/test_download.py +++ b/apps/predbat/tests/test_download.py @@ -95,13 +95,13 @@ def test_download(my_predbat): try: test_result = test_func(my_predbat) if test_result: - print(f"✗ FAILED: {test_name}") + print(f"FAIL: FAILED: {test_name}") failed += 1 else: - print(f"✓ PASSED: {test_name}") + print(f"PASS: PASSED: {test_name}") passed += 1 except Exception as e: - print(f"✗ EXCEPTION in {test_name}: {e}") + print(f"FAIL: EXCEPTION in {test_name}: {e}") import traceback traceback.print_exc() @@ -258,8 +258,8 @@ def _test_compute_file_sha1(my_predbat): Test Git blob SHA1 hash computation (matches GitHub's SHA) """ # Create a temporary file with known content - with tempfile.NamedTemporaryFile(mode="w", delete=False) as f: - f.write("test content\n") + with tempfile.NamedTemporaryFile(mode="wb", delete=False) as f: + f.write(b"test content\n") temp_path = f.name try: diff --git a/apps/predbat/tests/test_fetch_config_options.py b/apps/predbat/tests/test_fetch_config_options.py index 0fc67bb4c..aa3d269f0 100644 --- a/apps/predbat/tests/test_fetch_config_options.py +++ b/apps/predbat/tests/test_fetch_config_options.py @@ -153,7 +153,7 @@ def mock_expose_config(key, value): assert my_predbat.iboost_max_power == 0.05, "iboost_max_power should be 0.05 (3000 / 60000)" assert abs(my_predbat.iboost_min_power - 0.00833) < 0.001, "iboost_min_power should be ~0.00833 (500 / 60000)" - print("✓ Basic configuration test passed") + print("PASS: Basic configuration test passed") # Test 2: Axle control enabled with active event print("\n*** Test 2: Axle control enabled with active event ***") @@ -170,7 +170,7 @@ def mock_expose_config(key, value): assert my_predbat.set_read_only == True, "set_read_only should be True when Axle event is active" assert my_predbat.set_read_only_axle == True, "set_read_only_axle should be True when Axle event is active" - print("✓ Axle control with active event test passed") + print("PASS: Axle control with active event test passed") # Test 3: Axle control enabled but no active event print("\n*** Test 3: Axle control enabled but no active event ***") @@ -184,7 +184,7 @@ def mock_expose_config(key, value): assert my_predbat.set_read_only == False, "set_read_only should be False when no Axle event is active" assert my_predbat.set_read_only_axle == False, "set_read_only_axle should be False when no Axle event is active" - print("✓ Axle control with no active event test passed") + print("PASS: Axle control with no active event test passed") # Test 4: Manual read-only mode (overrides Axle) print("\n*** Test 4: Manual read-only mode (overrides Axle) ***") @@ -200,7 +200,7 @@ def mock_expose_config(key, value): assert my_predbat.set_read_only == True, "set_read_only should be True from manual setting" assert my_predbat.set_read_only_axle == False, "set_read_only_axle should be False when manual read-only is set" - print("✓ Manual read-only mode test passed") + print("PASS: Manual read-only mode test passed") # Test 5: Monitor mode configuration print("\n*** Test 5: Monitor mode configuration ***") @@ -220,7 +220,7 @@ def mock_expose_config(key, value): assert my_predbat.set_export_window == False, "set_export_window should be False in Monitor mode" assert my_predbat.set_soc_enable == False, "set_soc_enable should be False in Monitor mode" - print("✓ Monitor mode configuration test passed") + print("PASS: Monitor mode configuration test passed") # Test 6: Control SOC only mode print("\n*** Test 6: Control SOC only mode ***") @@ -236,7 +236,7 @@ def mock_expose_config(key, value): assert my_predbat.set_export_window == False, "set_export_window should be False" assert my_predbat.set_soc_enable == True, "set_soc_enable should be True" - print("✓ Control SOC only mode test passed") + print("PASS: Control SOC only mode test passed") # Test 7: Control charge mode print("\n*** Test 7: Control charge mode ***") @@ -252,7 +252,7 @@ def mock_expose_config(key, value): assert my_predbat.set_export_window == False, "set_export_window should be False" assert my_predbat.set_soc_enable == True, "set_soc_enable should be True" - print("✓ Control charge mode test passed") + print("PASS: Control charge mode test passed") # Test 8: Control charge & discharge mode print("\n*** Test 8: Control charge & discharge mode ***") @@ -268,7 +268,7 @@ def mock_expose_config(key, value): assert my_predbat.set_export_window == True, "set_export_window should be True" assert my_predbat.set_soc_enable == True, "set_soc_enable should be True" - print("✓ Control charge & discharge mode test passed") + print("PASS: Control charge & discharge mode test passed") # Test 9: Invalid mode defaults to Monitor print("\n*** Test 9: Invalid mode defaults to Monitor ***") @@ -281,7 +281,7 @@ def mock_expose_config(key, value): assert my_predbat.calculate_best_charge == False, "calculate_best_charge should be False for invalid mode" assert my_predbat.set_soc_enable == False, "set_soc_enable should be False for invalid mode" - print("✓ Invalid mode defaults to Monitor test passed") + print("PASS: Invalid mode defaults to Monitor test passed") # Test 10: Holiday mode override print("\n*** Test 10: Holiday mode configuration ***") @@ -297,7 +297,7 @@ def mock_expose_config(key, value): assert my_predbat.days_previous == [1], "days_previous should be [1] in holiday mode" assert my_predbat.max_days_previous == 2, "max_days_previous should be 2 (1 + 1) in holiday mode" - print("✓ Holiday mode configuration test passed") + print("PASS: Holiday mode configuration test passed") # Test 11: Forecast calculations print("\n*** Test 11: Forecast calculations ***") @@ -312,7 +312,7 @@ def mock_expose_config(key, value): assert my_predbat.forecast_days == 2, "forecast_days should be 2 for 25 hours" assert my_predbat.forecast_minutes == 25 * 60, "forecast_minutes should be 1500" - print("✓ Forecast calculations test passed") + print("PASS: Forecast calculations test passed") # Test 12: LoadML should not modify the in-day adjustment config setting print("\n*** Test 12: LoadML does not change in-day adjustment config setting ***") @@ -327,7 +327,7 @@ def mock_expose_config(key, value): assert my_predbat.calculate_inday_adjustment is True, "calculate_inday_adjustment config should remain unchanged" assert ("calculate_inday_adjustment", False) not in exposed_config_calls, "calculate_inday_adjustment should not be force-updated in config" - print("✓ LoadML config-preservation test passed") + print("PASS: LoadML config-preservation test passed") # Test 13: car_charging_planned/now response matching is case-insensitive print("\n*** Test 13: car_charging_planned/now response case-insensitivity ***") diff --git a/apps/predbat/tests/test_fetch_pv_forecast.py b/apps/predbat/tests/test_fetch_pv_forecast.py index a257150df..f190116ab 100644 --- a/apps/predbat/tests/test_fetch_pv_forecast.py +++ b/apps/predbat/tests/test_fetch_pv_forecast.py @@ -111,11 +111,11 @@ def test_fetch_pv_forecast_with_relative_time(): # With the corrected formula (target = stored_minute - offset), a +120 min offset # (relative_time is 2 hours before midnight_utc) shifts data BACK by 120 minutes: - # forecast minute 0 (= relative_time + 0 = midnight - 2h) → output minute -120 - # forecast minute 60 (= midnight - 1h) → output minute -60 - # forecast minute 120 (= midnight exactly) → output minute 0 - # forecast minute 180 (= 1h after midnight) → output minute 60 - # forecast minute 240 (= 2h after midnight) → output minute 120 + # forecast minute 0 (= relative_time + 0 = midnight - 2h) -> output minute -120 + # forecast minute 60 (= midnight - 1h) -> output minute -60 + # forecast minute 120 (= midnight exactly) -> output minute 0 + # forecast minute 180 (= 1h after midnight) -> output minute 60 + # forecast minute 240 (= 2h after midnight) -> output minute 120 # Verify the offset is correctly applied assert pv_forecast_minute[0] == 1.0, f"Expected minute 0 to be 1.0, got {pv_forecast_minute.get(0)}" @@ -127,9 +127,9 @@ def test_fetch_pv_forecast_with_relative_time(): assert pv_forecast_minute10[60] == 1.2, f"Expected forecast10 minute 60 to be 1.2, got {pv_forecast_minute10.get(60)}" assert pv_forecast_minute10[120] == 1.6, f"Expected forecast10 minute 120 to be 1.6, got {pv_forecast_minute10.get(120)}" - print(f"✓ Forecast data correctly shifted back by 120 minutes (stored_minute - offset)") - print(f"✓ pv_forecast_minute[0]={pv_forecast_minute[0]}, [60]={pv_forecast_minute[60]}, [120]={pv_forecast_minute[120]}") - print(f"✓ pv_forecast_minute10[0]={pv_forecast_minute10[0]}, [60]={pv_forecast_minute10[60]}, [120]={pv_forecast_minute10[120]}") + print(f"PASS: Forecast data correctly shifted back by 120 minutes (stored_minute - offset)") + print(f"PASS: pv_forecast_minute[0]={pv_forecast_minute[0]}, [60]={pv_forecast_minute[60]}, [120]={pv_forecast_minute[120]}") + print(f"PASS: pv_forecast_minute10[0]={pv_forecast_minute10[0]}, [60]={pv_forecast_minute10[60]}, [120]={pv_forecast_minute10[120]}") print("Test 1 PASSED") @@ -185,8 +185,8 @@ def test_fetch_pv_forecast_no_relative_time(): assert pv_forecast_minute10[120] == 0.8, f"Expected forecast10 minute 120 to be 0.8, got {pv_forecast_minute10[120]}" assert pv_forecast_minute10[180] == 1.2, f"Expected forecast10 minute 180 to be 1.2, got {pv_forecast_minute10[180]}" - print(f"✓ Forecast data used with no offset (relative_time missing)") - print(f"✓ pv_forecast_minute[0]={pv_forecast_minute[0]}, [60]={pv_forecast_minute[60]}, [120]={pv_forecast_minute[120]}") + print(f"PASS: Forecast data used with no offset (relative_time missing)") + print(f"PASS: pv_forecast_minute[0]={pv_forecast_minute[0]}, [60]={pv_forecast_minute[60]}, [120]={pv_forecast_minute[120]}") print("Test 2 PASSED") @@ -234,8 +234,8 @@ def test_fetch_pv_forecast_invalid_relative_time(): assert pv_forecast_minute[60] == 0.5, f"Expected minute 60 to be 0.5, got {pv_forecast_minute[60]}" assert pv_forecast_minute[120] == 1.0, f"Expected minute 120 to be 1.0, got {pv_forecast_minute[120]}" - print(f"✓ Forecast data used with no offset (invalid relative_time)") - print(f"✓ pv_forecast_minute[0]={pv_forecast_minute[0]}, [60]={pv_forecast_minute[60]}, [120]={pv_forecast_minute[120]}") + print(f"PASS: Forecast data used with no offset (invalid relative_time)") + print(f"PASS: pv_forecast_minute[0]={pv_forecast_minute[0]}, [60]={pv_forecast_minute[60]}, [120]={pv_forecast_minute[120]}") print("Test 3 PASSED") @@ -287,8 +287,8 @@ def test_fetch_pv_forecast_relative_time_same_as_midnight(): assert pv_forecast_minute[120] == 1.0, f"Expected minute 120 to be 1.0, got {pv_forecast_minute[120]}" assert pv_forecast_minute[180] == 1.5, f"Expected minute 180 to be 1.5, got {pv_forecast_minute[180]}" - print(f"✓ Forecast data maps directly when relative_time = midnight_utc") - print(f"✓ pv_forecast_minute[0]={pv_forecast_minute[0]}, [60]={pv_forecast_minute[60]}, [120]={pv_forecast_minute[120]}") + print(f"PASS: Forecast data maps directly when relative_time = midnight_utc") + print(f"PASS: pv_forecast_minute[0]={pv_forecast_minute[0]}, [60]={pv_forecast_minute[60]}, [120]={pv_forecast_minute[120]}") print("Test 4 PASSED") @@ -343,12 +343,12 @@ def test_fetch_pv_forecast_previous_day(): # With the corrected formula (target = stored_minute - offset), a +1440 min offset # maps yesterday's stored minutes to today-relative minutes: - # forecast minute 0 (yesterday midnight) → output minute -1440 (past, before today midnight) - # forecast minute 360 (yesterday 6am) → output minute -1080 (past) - # forecast minute 720 (yesterday noon) → output minute -720 (past) - # forecast minute 1080 (yesterday 6pm) → output minute -360 (past) - # forecast minute 1440 (today midnight) → output minute 0 ← first visible today slot - # forecast minute 1500 (today 1am) → output minute 60 + # forecast minute 0 (yesterday midnight) -> output minute -1440 (past, before today midnight) + # forecast minute 360 (yesterday 6am) -> output minute -1080 (past) + # forecast minute 720 (yesterday noon) -> output minute -720 (past) + # forecast minute 1080 (yesterday 6pm) -> output minute -360 (past) + # forecast minute 1440 (today midnight) -> output minute 0 <- first visible today slot + # forecast minute 1500 (today 1am) -> output minute 60 # Verify the offset is correctly applied # Today midnight (forecast minute 1440) should appear at output minute 0 @@ -360,8 +360,8 @@ def test_fetch_pv_forecast_previous_day(): assert pv_forecast_minute10[0] == 1.6, f"Expected forecast10 minute 0 to be 1.6, got {pv_forecast_minute10.get(0)}" assert pv_forecast_minute10[60] == 2.0, f"Expected forecast10 minute 60 to be 2.0, got {pv_forecast_minute10.get(60)}" - print(f"✓ Forecast data correctly shifted back by 1440 minutes (stored_minute - offset)") - print(f"✓ pv_forecast_minute[0]={pv_forecast_minute[0]}, [60]={pv_forecast_minute[60]}") + print(f"PASS: Forecast data correctly shifted back by 1440 minutes (stored_minute - offset)") + print(f"PASS: pv_forecast_minute[0]={pv_forecast_minute[0]}, [60]={pv_forecast_minute[60]}") print("Test 5 PASSED") @@ -412,10 +412,10 @@ def test_fetch_pv_forecast_negative_offset(): # With the corrected formula (target = stored_minute - offset), a -60 min offset # (relative_time is 1 hour AFTER midnight_utc) shifts data FORWARD by 60 minutes: - # forecast minute 60 (= relative_time + 1h = 2h after midnight) → output minute 120 - # forecast minute 120 (= 3h after midnight) → output minute 180 - # forecast minute 180 (= 4h after midnight) → output minute 240 - # forecast minute 240 (= 5h after midnight) → output minute 300 + # forecast minute 60 (= relative_time + 1h = 2h after midnight) -> output minute 120 + # forecast minute 120 (= 3h after midnight) -> output minute 180 + # forecast minute 180 (= 4h after midnight) -> output minute 240 + # forecast minute 240 (= 5h after midnight) -> output minute 300 # Check that data is mapped correctly with negative offset assert pv_forecast_minute[120] == 0.0, f"Expected minute 120 to be 0.0, got {pv_forecast_minute.get(120)}" @@ -429,8 +429,8 @@ def test_fetch_pv_forecast_negative_offset(): assert pv_forecast_minute10[240] == 0.8, f"Expected forecast10 minute 240 to be 0.8, got {pv_forecast_minute10.get(240)}" assert pv_forecast_minute10[300] == 1.2, f"Expected forecast10 minute 300 to be 1.2, got {pv_forecast_minute10.get(300)}" - print(f"✓ Forecast data correctly shifted forward by 60 minutes (stored_minute - (-60))") - print(f"✓ pv_forecast_minute[120]={pv_forecast_minute[120]}, [180]={pv_forecast_minute[180]}, [240]={pv_forecast_minute[240]}") + print(f"PASS: Forecast data correctly shifted forward by 60 minutes (stored_minute - (-60))") + print(f"PASS: pv_forecast_minute[120]={pv_forecast_minute[120]}, [180]={pv_forecast_minute[180]}, [240]={pv_forecast_minute[240]}") print("Test 6 PASSED") diff --git a/apps/predbat/tests/test_fill_load_from_power.py b/apps/predbat/tests/test_fill_load_from_power.py index 46c5701df..1ff89f22a 100644 --- a/apps/predbat/tests/test_fill_load_from_power.py +++ b/apps/predbat/tests/test_fill_load_from_power.py @@ -82,7 +82,7 @@ def test_fill_load_from_power_basic(): total_consumption = result[0] - result[59] assert abs(total_consumption - 3.0) < 0.2, f"Total consumption should be near 3.0 kWh, got {dp4(total_consumption)} kWh" - print(f"✓ Two 30-minute periods (0-59): {dp4(result[0])} -> {dp4(result[59])}, consumption: {dp4(total_consumption)} kWh") + print(f"PASS: Two 30-minute periods (0-59): {dp4(result[0])} -> {dp4(result[59])}, consumption: {dp4(total_consumption)} kWh") print("Test 1 PASSED") @@ -119,7 +119,7 @@ def test_fill_load_from_power_no_power_data(): # Check warning was logged assert any("No power data" in msg for msg in fetch.log_messages), "Should log warning about no power data" - print(f"✓ Original data returned: {dp4(result[0])}, {dp4(result[4])}") + print(f"PASS: Original data returned: {dp4(result[0])}, {dp4(result[4])}") print("Test 2 PASSED") @@ -162,7 +162,7 @@ def test_fill_load_from_power_partial_power_data(): total_consumption = result[0] - result[59] assert abs(total_consumption - 4.0) < 0.2, f"Total consumption should be near 4.0 kWh, got {dp4(total_consumption)} kWh" - print(f"✓ Two 30-minute periods: Power in first period, distributed in second") + print(f"PASS: Two 30-minute periods: Power in first period, distributed in second") print(f" {dp4(result[0])} -> {dp4(result[29])} -> {dp4(result[59])}") print("Test 3 PASSED") @@ -197,7 +197,7 @@ def test_fill_load_from_power_single_minute_period(): # Last value should be around 4.0 assert abs(result[9] - 4.0) < 0.1, f"Minute 9: expected ~4.0, got {result[9]}" - print(f"✓ Short span handled: {dp4(result[0])} -> {dp4(result[9])}") + print(f"PASS: Short span handled: {dp4(result[0])} -> {dp4(result[9])}") print("Test 4 PASSED") @@ -225,7 +225,7 @@ def test_fill_load_from_power_zero_load(): for minute in range(0, 30): assert result[minute] == 0.0, f"Minute {minute} should be 0.0, got {result[minute]}" - print("✓ Zero load values preserved") + print("PASS: Zero load values preserved") print("Test 5 PASSED") @@ -301,9 +301,9 @@ def test_fill_load_from_power_backwards_time(): first_hour_consumption = result[0] - result[59] assert abs(first_hour_consumption - 1.5) < 0.5, f"First hour should have ~1.5 kWh consumption, got {dp4(first_hour_consumption)}" - print(f"✓ Backwards time: minute 0 (now) = {dp4(result[0])} kWh") - print(f"✓ Backwards time: minute 60 (1h ago) = {dp4(result[60])} kWh") - print(f"✓ Backwards time: minute 120 (2h ago) = {dp4(result[120])} kWh") + print(f"PASS: Backwards time: minute 0 (now) = {dp4(result[0])} kWh") + print(f"PASS: Backwards time: minute 60 (1h ago) = {dp4(result[60])} kWh") + print(f"PASS: Backwards time: minute 120 (2h ago) = {dp4(result[120])} kWh") print("Test 6 PASSED") @@ -346,7 +346,7 @@ def test_fill_load_from_power_data_extends_beyond_load(): # They may not exist in the result dict at all, which is the correct behaviour. assert 1440 not in result or result[1440] == 0, "Minute 1440 should not be populated from power-only range" - print("✓ No spurious zero-load warning when power data extends past load data range") + print("PASS: No spurious zero-load warning when power data extends past load data range") print("Test 7 PASSED") @@ -378,7 +378,7 @@ def test_fill_load_from_power_negative_power_corrupts_load_total(): for minute in range(0, 31): load_minutes[minute] = 10.0 for minute in range(31, 61): - load_minutes[minute] = 10.0 - (minute - 30) * (2.0 / 30) # 9.933 → 8.0 + load_minutes[minute] = 10.0 - (minute - 30) * (2.0 / 30) # 9.933 -> 8.0 expected_total = load_minutes[0] - load_minutes[60] # 10.0 - 8.0 = 2.0 kWh @@ -425,17 +425,17 @@ def run_all_tests(my_predbat=None): test_fill_load_from_power_negative_power_corrupts_load_total() print("\n" + "=" * 60) - print("✅ ALL TESTS PASSED") + print("PASS: ALL TESTS PASSED") print("=" * 60) return 0 # Return 0 for success except AssertionError as e: print("\n" + "=" * 60) - print(f"❌ TEST FAILED: {e}") + print(f"ERROR: TEST FAILED: {e}") print("=" * 60) return 1 # Return 1 for failure except Exception as e: print("\n" + "=" * 60) - print(f"❌ ERROR: {e}") + print(f"ERROR: ERROR: {e}") import traceback traceback.print_exc() diff --git a/apps/predbat/tests/test_find_battery_size.py b/apps/predbat/tests/test_find_battery_size.py index c5b132e6d..30a11733b 100644 --- a/apps/predbat/tests/test_find_battery_size.py +++ b/apps/predbat/tests/test_find_battery_size.py @@ -547,7 +547,7 @@ def test_battery_scaling_auto_basic(my_predbat): day0 = str(today - td(days=3)) day1 = str(today - td(days=2)) day2 = str(today - td(days=1)) - # Values: 9.0, 9.5, 10.0; today adds 9.4 → trimmed mean of [9.0,9.4,9.5,10.0] drops extremes → (9.4+9.5)/2 = 9.45 + # Values: 9.0, 9.5, 10.0; today adds 9.4 -> trimmed mean of [9.0,9.4,9.5,10.0] drops extremes -> (9.4+9.5)/2 = 9.45 existing_history = {day0: 9.0, day1: 9.5, day2: 10.0} my_predbat.ha_interface.dummy_items[sensor_name] = {"state": "9.5", "history": existing_history} @@ -627,7 +627,7 @@ def test_battery_scaling_auto_clamping(my_predbat): my_predbat.battery_scaling_auto = True sensor_name = "sensor.{}_soc_max_calculated".format(my_predbat.prefix) - # Test lower clamp: find_battery_size returns 7.0, ratio 0.7 → clamped to 0.8 → soc_max=8.0 + # Test lower clamp: find_battery_size returns 7.0, ratio 0.7 -> clamped to 0.8 -> soc_max=8.0 inv = _make_inv_for_scaling(my_predbat, nominal) my_predbat.ha_interface.dummy_items.pop(sensor_name, None) inv.find_battery_size = lambda _nc=0: 7.0 @@ -642,7 +642,7 @@ def test_battery_scaling_auto_clamping(my_predbat): else: print("SUCCESS: lower clamp to 0.8 correct") - # Test upper clamp: find_battery_size returns 11.0, ratio 1.1 → clamped to 1.0 → soc_max=10.0 + # Test upper clamp: find_battery_size returns 11.0, ratio 1.1 -> clamped to 1.0 -> soc_max=10.0 inv2 = _make_inv_for_scaling(my_predbat, nominal) my_predbat.ha_interface.dummy_items.pop(sensor_name, None) inv2.find_battery_size = lambda _nc=0: 11.0 @@ -742,7 +742,7 @@ def mock_find_should_not_be_called(): if calls[0] > 0: print("ERROR: find_battery_size was called {} time(s) but should have been skipped".format(calls[0])) failed = True - # stored mean=9.0, nominal=10.0 → scaling=0.9 → soc_max=9.0 + # stored mean=9.0, nominal=10.0 -> scaling=0.9 -> soc_max=9.0 expected_scaling = _clamped_auto_scaling(9.0, nominal) expected_soc_max = round(nominal * expected_scaling, 3) if abs(inv.soc_max - expected_soc_max) > 0.001: @@ -1062,7 +1062,7 @@ def test_battery_size_tracking_no_nominal(my_predbat): try: inv.battery_size_tracking() - # nominal_in_sensor=0 \u2192 use trimmed_mean directly without clamping + # nominal_in_sensor=0 -> use trimmed_mean directly without clamping expected_soc_max = round(9.2, 3) if abs(inv.soc_max - expected_soc_max) > 0.001: print("ERROR: soc_max {} does not match expected {:.3f} (no-nominal path)".format(inv.soc_max, expected_soc_max)) @@ -1201,7 +1201,7 @@ def test_update_soc_max_calculated_sensor_all_none(my_predbat): # (so the HA entity shows a sensible value rather than 'unknown') sensor_state = my_predbat.ha_interface.dummy_items.get(sensor_name, {}) state_value = sensor_state.get("state") if isinstance(sensor_state, dict) else sensor_state - expected_state = nominal # nominal_capacity > 0 → state = nominal_capacity + expected_state = nominal # nominal_capacity > 0 -> state = nominal_capacity if state_value != expected_state: print("ERROR: expected sensor state {}, got '{}'".format(expected_state, state_value)) failed = True @@ -1248,8 +1248,8 @@ def test_update_soc_max_calculated_sensor_mixed_none(my_predbat): from datetime import timedelta as td - # 2 valid days + 1 None day; calling with a new valid value → 3 real values total - # values: 9.0, 9.5, 9.2 → sorted [9.0, 9.2, 9.5] → trimmed mean = 9.2 + # 2 valid days + 1 None day; calling with a new valid value -> 3 real values total + # values: 9.0, 9.5, 9.2 -> sorted [9.0, 9.2, 9.5] -> trimmed mean = 9.2 today = my_predbat.now_utc.date() existing_history = { str(today - td(days=3)): 9.0, @@ -1264,7 +1264,7 @@ def test_update_soc_max_calculated_sensor_mixed_none(my_predbat): print("ERROR: expected a valid mean when real values exist alongside None entries") failed = True else: - # Sorted real values: [9.0, 9.2, 9.5] → trimmed drops 9.0 and 9.5 → mean=9.2 + # Sorted real values: [9.0, 9.2, 9.5] -> trimmed drops 9.0 and 9.5 -> mean=9.2 expected_mean = 9.2 if abs(result - expected_mean) > 0.05: print("ERROR: expected trimmed mean ~{:.2f}, got {:.3f}".format(expected_mean, result)) diff --git a/apps/predbat/tests/test_find_charge_window.py b/apps/predbat/tests/test_find_charge_window.py index bc8814dcc..fe0a87b2b 100644 --- a/apps/predbat/tests/test_find_charge_window.py +++ b/apps/predbat/tests/test_find_charge_window.py @@ -28,16 +28,16 @@ def test_find_charge_window(my_predbat): """ Comprehensive tests for find_charge_window covering all code paths: - Path A - mixed rate exceeds combine_rate_threshold → window closed early - Path B - combine_export_slots=False hits export_slot_split → split - Path C - combine_charge_slots=False hits charge_slot_split → split + Path A - mixed rate exceeds combine_rate_threshold -> window closed early + Path B - combine_export_slots=False hits export_slot_split -> split + Path C - combine_charge_slots=False hits charge_slot_split -> split Path D - manual_all_times slot split at plan_interval_minutes Path E - alt_rates alternate_rate_boundary terminates export window; also 24-hour export cap Path F - window start (rate_low_start set for first time) Path G - window continuation and correct average calculation - Path H - rate outside threshold while window in progress → closes window - Path I - gap in rates while window in progress → closes window (regression + Path H - rate outside threshold while window in progress -> closes window + Path I - gap in rates while window in progress -> closes window (regression for previous break-too-early bug) Misc - no qualifying rates, find_high=False, find_high=True, zero rate excluded for find_high, scan from non-zero minute, scan stops at @@ -70,7 +70,7 @@ def test_find_charge_window(my_predbat): scan_end = my_predbat.forecast_minutes + my_predbat.minutes_now + 12 * 60 # ----------------------------------------------------------------------- - # Test: no rates → no window found + # Test: no rates -> no window found # ----------------------------------------------------------------------- print("Test find_charge_window: empty rates dict returns no window") s, e, avg = my_predbat.find_charge_window({}, 0, thresh_lo, find_high=False) @@ -80,7 +80,7 @@ def test_find_charge_window(my_predbat): # ----------------------------------------------------------------------- # Test: simple charge window (find_high=False) — Path F + G - # Rates start at 0, low for 120 min, high after → window [0, 120] + # Rates start at 0, low for 120 min, high after -> window [0, 120] # ----------------------------------------------------------------------- print("Test find_charge_window: simple charge window (find_high=False)") rates = {} @@ -94,7 +94,7 @@ def test_find_charge_window(my_predbat): # ----------------------------------------------------------------------- # Test: simple export window (find_high=True) — Path F + G - # Rates start at 0, high for 120 min, low after → window [0, 120] + # Rates start at 0, high for 120 min, low after -> window [0, 120] # ----------------------------------------------------------------------- print("Test find_charge_window: simple export window (find_high=True)") rates2 = {} @@ -118,7 +118,7 @@ def test_find_charge_window(my_predbat): # ----------------------------------------------------------------------- # Test: rate goes ABOVE threshold while window in progress — Path H - # Low rates 0..59, high rate at 60.. → window closes at 60 + # Low rates 0..59, high rate at 60.. -> window closes at 60 # ----------------------------------------------------------------------- print("Test find_charge_window: rate above threshold closes window (Path H)") rates_h = {} @@ -133,7 +133,7 @@ def test_find_charge_window(my_predbat): # ----------------------------------------------------------------------- # Test: Path A — combine_rate_threshold: second rate too different # Low rate 5 for 0..55, then rate 8 at 60 (diff = 3 > threshold 1.0) - # → window closes at 60 due to combine_rate_threshold + # -> window closes at 60 due to combine_rate_threshold # ----------------------------------------------------------------------- print("Test find_charge_window: combine_rate_threshold closes mixed window (Path A)") my_predbat.combine_rate_threshold = 2.0 # allow up to 2p difference @@ -149,7 +149,7 @@ def test_find_charge_window(my_predbat): # ----------------------------------------------------------------------- # Test: Path C — combine_charge_slots=False splits charge at charge_slot_split - # Low rates from 0 to 120, charge_slot_split=30 → window [0, 30] + # Low rates from 0 to 120, charge_slot_split=30 -> window [0, 30] # ----------------------------------------------------------------------- print("Test find_charge_window: combine_charge_slots=False splits slot (Path C)") my_predbat.combine_charge_slots = False @@ -162,7 +162,7 @@ def test_find_charge_window(my_predbat): # ----------------------------------------------------------------------- # Test: Path B — combine_export_slots=False splits export at export_slot_split - # High rates from 0 to 120, export_slot_split=30 → window [0, 30] + # High rates from 0 to 120, export_slot_split=30 -> window [0, 30] # ----------------------------------------------------------------------- print("Test find_charge_window: combine_export_slots=False splits slot (Path B)") my_predbat.combine_export_slots = False @@ -176,7 +176,7 @@ def test_find_charge_window(my_predbat): # ----------------------------------------------------------------------- # Test: Path D — manual_all_times slot split at plan_interval_minutes # rate_low_start=0 is in manual_all_times; low rates from 0 to 120; - # plan_interval_minutes=30 → window closes at minute 30 + # plan_interval_minutes=30 -> window closes at minute 30 # ----------------------------------------------------------------------- print("Test find_charge_window: manual_all_times split at plan_interval_minutes (Path D)") my_predbat.plan_interval_minutes = 30 @@ -190,14 +190,14 @@ def test_find_charge_window(my_predbat): # ----------------------------------------------------------------------- # Test: Path E — alternate_rate_boundary splits export window - # find_high=True; alt_rates changes significantly between minutes 25→30; + # find_high=True; alt_rates changes significantly between minutes 25->30; # high export rates throughout; plan_interval_minutes=30; - # → export window should end at 30 (boundary reached after plan_interval) + # -> export window should end at 30 (boundary reached after plan_interval) # ----------------------------------------------------------------------- print("Test find_charge_window: alternate_rate_boundary splits export window (Path E)") my_predbat.plan_interval_minutes = 30 my_predbat.export_slot_split = 30 - # alt_rates min=0, max=20 → alt_rate_threshold=2.0; jump of 10 qualifies + # alt_rates min=0, max=20 -> alt_rate_threshold=2.0; jump of 10 qualifies alt_rates_e = {} for m in range(0, 30, 5): alt_rates_e[m] = 0.0 # low alt rate @@ -207,7 +207,7 @@ def test_find_charge_window(my_predbat): rates_e = {m: high_rate for m in range(0, scan_end, 5)} # all qualify # Window starts at 0; alternate_rate_boundary is set at minute 30; - # at minute 30, (30 - 0) = 30 >= plan_interval_minutes=30 → break + # at minute 30, (30 - 0) = 30 >= plan_interval_minutes=30 -> break s, e, _ = my_predbat.find_charge_window(rates_e, 0, thresh_hi, find_high=True, alt_rates=alt_rates_e) failed |= _assert_window("path E alt rate boundary", s, e, _, 0, 30) @@ -224,7 +224,7 @@ def test_find_charge_window(my_predbat): # ----------------------------------------------------------------------- # Test: correct average over varying rates (Path G) - # Three rate bands: 4, 6, 8 each for 1 slot (5 min) → average = (4+6+8)/3 = 6 + # Three rate bands: 4, 6, 8 each for 1 slot (5 min) -> average = (4+6+8)/3 = 6 # Raise combine_rate_threshold so all three rates are combined into one window. # ----------------------------------------------------------------------- print("Test find_charge_window: correct average calculation (Path G)") diff --git a/apps/predbat/tests/test_futurerate_auto.py b/apps/predbat/tests/test_futurerate_auto.py index 5621d35eb..9a667edd5 100644 --- a/apps/predbat/tests/test_futurerate_auto.py +++ b/apps/predbat/tests/test_futurerate_auto.py @@ -323,7 +323,7 @@ def _test_futurerate_auto_disabled_proceeds(my_predbat): def _test_futurerate_auto_no_agile_returns_empty(my_predbat): - """futurerate_adjust_auto=True + no Agile detected → return ({}, {}) without hitting the API.""" + """futurerate_adjust_auto=True + no Agile detected -> return ({}, {}) without hitting the API.""" base = MockFutureRateBase() base.args["futurerate_adjust_auto"] = True future = _make_future_rate(base) @@ -341,7 +341,7 @@ def _test_futurerate_auto_no_agile_returns_empty(my_predbat): def _test_futurerate_auto_import_agile_proceeds(my_predbat): - """futurerate_adjust_auto=True + import is Agile → calls futurerate_analysis_new.""" + """futurerate_adjust_auto=True + import is Agile -> calls futurerate_analysis_new.""" base = MockFutureRateBase() base.args["futurerate_adjust_auto"] = True base.args["metric_octopus_import"] = IMPORT_ENTITY @@ -361,7 +361,7 @@ def _test_futurerate_auto_import_agile_proceeds(my_predbat): def _test_futurerate_auto_export_agile_proceeds(my_predbat): - """futurerate_adjust_auto=True + only export is Agile → calls futurerate_analysis_new.""" + """futurerate_adjust_auto=True + only export is Agile -> calls futurerate_analysis_new.""" base = MockFutureRateBase() base.args["futurerate_adjust_auto"] = True base.args["metric_octopus_export"] = EXPORT_ENTITY @@ -386,7 +386,7 @@ def _test_futurerate_auto_export_agile_proceeds(my_predbat): def _test_futurerate_adjust_auto_import_agile_calibrates_import(my_predbat): - """futurerate_adjust_auto=True + import Agile → calibrate import with real rates.""" + """futurerate_adjust_auto=True + import Agile -> calibrate import with real rates.""" base = MockFutureRateBase() base.args["futurerate_adjust_auto"] = True base.args["futurerate_adjust_import"] = False @@ -432,7 +432,7 @@ def mock_calibrate(real_mdata, mdata, is_import, peak_start_minutes, peak_end_mi def _test_futurerate_adjust_auto_export_agile_calibrates_export(my_predbat): - """futurerate_adjust_auto=True + export Agile → calibrate export with real rates.""" + """futurerate_adjust_auto=True + export Agile -> calibrate export with real rates.""" base = MockFutureRateBase() base.args["futurerate_adjust_auto"] = True base.args["futurerate_adjust_import"] = False @@ -476,7 +476,7 @@ def mock_calibrate(real_mdata, mdata, is_import, peak_start_minutes, peak_end_mi def _test_futurerate_adjust_auto_disabled_uses_manual_flags(my_predbat): - """futurerate_adjust_auto=False → manual futurerate_adjust_import/export flags respected.""" + """futurerate_adjust_auto=False -> manual futurerate_adjust_import/export flags respected.""" base = MockFutureRateBase() base.args["futurerate_adjust_auto"] = False base.args["futurerate_adjust_import"] = True diff --git a/apps/predbat/tests/test_gateway.py b/apps/predbat/tests/test_gateway.py index aea4e45fd..48f614f11 100644 --- a/apps/predbat/tests/test_gateway.py +++ b/apps/predbat/tests/test_gateway.py @@ -22,7 +22,7 @@ def approx_equal(actual, expected, abs_tol=0.01): class TestProtobufDecode: - """Test protobuf telemetry → entity mapping.""" + """Test protobuf telemetry -> entity mapping.""" def _make_status(self, soc=50, battery_power=1000, pv_power=2000, grid_power=-500, load_power=1500, mode=0): status = pb.GatewayStatus() @@ -300,7 +300,7 @@ def _make_gateway(self): gw._last_status = None gw.args = {} gw.local_tz = pytz.timezone("Europe/London") - gw._dashboard_calls = {} # entity_id → (state, attributes) + gw._dashboard_calls = {} # entity_id -> (state, attributes) def capture_dashboard(entity_id, state=None, attributes=None, app=None): gw._dashboard_calls[entity_id] = (state, attributes) @@ -386,7 +386,7 @@ def test_inverter_time_sensor(self): status = self._make_status() gw._inject_entities(status) - # Serial "CE123456789" (len > 6) → last 6 chars lowercase = "456789" + # Serial "CE123456789" (len > 6) -> last 6 chars lowercase = "456789" entity = "sensor.predbat_gateway_456789_inverter_time" assert entity in gw._dashboard_calls state, attrs = gw._dashboard_calls[entity] @@ -414,7 +414,7 @@ def test_non_primary_inverter_skipped(self): assert "sensor.predbat_gateway_000001_soc" in gw._dashboard_calls def test_battery_power_negated(self): - """Battery power sign is inverted: firmware +ve=charging → PredBat +ve=discharging.""" + """Battery power sign is inverted: firmware +ve=charging -> PredBat +ve=discharging.""" gw = self._make_gateway() gw._inject_entities(self._make_status(battery_power=1000)) @@ -449,20 +449,20 @@ def test_schedule_select_entities(self): gw._inject_entities(self._make_status()) suffix = "456789" - # charge_start = 130 → 01:30:00 + # charge_start = 130 -> 01:30:00 state, attrs = gw._dashboard_calls[f"select.predbat_gateway_{suffix}_charge_slot1_start"] assert state == "01:30:00" assert attrs == GATEWAY_ATTRIBUTE_TABLE.get("charge_slot1_start", {}) - # charge_end = 430 → 04:30:00 + # charge_end = 430 -> 04:30:00 state, _ = gw._dashboard_calls[f"select.predbat_gateway_{suffix}_charge_slot1_end"] assert state == "04:30:00" - # discharge_start = 1600 → 16:00:00 + # discharge_start = 1600 -> 16:00:00 state, _ = gw._dashboard_calls[f"select.predbat_gateway_{suffix}_discharge_slot1_start"] assert state == "16:00:00" - # discharge_end = 1900 → 19:00:00 + # discharge_end = 1900 -> 19:00:00 state, _ = gw._dashboard_calls[f"select.predbat_gateway_{suffix}_discharge_slot1_end"] assert state == "19:00:00" @@ -1256,7 +1256,7 @@ def test_republish_refreshes_timestamp(self): second_plan.ParseFromString(second_payload) assert second_plan.timestamp >= first_plan.timestamp # rebuilt with current clock - assert second_plan.plan_version == first_plan.plan_version # content unchanged → same version + assert second_plan.plan_version == first_plan.plan_version # content unchanged -> same version # The cached bytes are refreshed so subsequent reads reflect the new timestamp. assert gw._last_plan_data == second_payload @@ -1898,7 +1898,7 @@ def test_discharge_slot_end(self): # ------------------------------------------------------------------ def test_midnight_time_converts_correctly(self): - """00:00:00 → HHMM 0 (midnight).""" + """00:00:00 -> HHMM 0 (midnight).""" import json gw = self._make_gateway() @@ -1949,7 +1949,7 @@ def test_no_command_when_suffix_not_in_map(self): class TestNumberEvent: - """Tests for GatewayMQTT.number_event() — numeric entity → command routing.""" + """Tests for GatewayMQTT.number_event() — numeric entity -> command routing.""" def _make_gateway(self): from gateway import GatewayMQTT @@ -1980,25 +1980,25 @@ def _run(self, coro): # ------------------------------------------------------------------ def test_charge_rate_routes_correctly(self): - """charge_rate entity → set_charge_rate with power_w.""" + """charge_rate entity -> set_charge_rate with power_w.""" gw = self._make_gateway() self._run(gw.number_event("number.predbat_gateway_456789_charge_rate", "3000")) assert gw._published == [("set_charge_rate", {"power_w": 3000, "serial": "CE123456789"})] def test_discharge_rate_routes_correctly(self): - """discharge_rate entity → set_discharge_rate with power_w (not charge_rate).""" + """discharge_rate entity -> set_discharge_rate with power_w (not charge_rate).""" gw = self._make_gateway() self._run(gw.number_event("number.predbat_gateway_456789_discharge_rate", "2500")) assert gw._published == [("set_discharge_rate", {"power_w": 2500, "serial": "CE123456789"})] def test_reserve_soc_routes_correctly(self): - """reserve_soc entity → set_reserve with target_soc.""" + """reserve_soc entity -> set_reserve with target_soc.""" gw = self._make_gateway() self._run(gw.number_event("number.predbat_gateway_456789_reserve_soc", "10")) assert gw._published == [("set_reserve", {"target_soc": 10, "serial": "CE123456789"})] def test_target_soc_routes_correctly(self): - """target_soc entity → set_target_soc with target_soc.""" + """target_soc entity -> set_target_soc with target_soc.""" gw = self._make_gateway() self._run(gw.number_event("number.predbat_gateway_456789_target_soc", "100")) assert gw._published == [("set_target_soc", {"target_soc": 100, "serial": "CE123456789"})] @@ -2079,7 +2079,7 @@ def test_no_command_when_suffix_not_in_map(self): class TestSwitchEvent: - """Tests for GatewayMQTT.switch_event() — charge/discharge enable → mode commands.""" + """Tests for GatewayMQTT.switch_event() — charge/discharge enable -> mode commands.""" def _make_gateway(self): from gateway import GatewayMQTT @@ -2130,7 +2130,7 @@ def test_charge_enabled_turn_off(self): def test_charge_enabled_toggle(self): """Toggling charge_enabled flips based on current state from get_state_wrapper.""" gw = self._make_gateway() - # currently on → toggle → off + # currently on -> toggle -> off gw._state["switch.predbat_gateway_456789_charge_enabled"] = True self._run(gw.switch_event("switch.predbat_gateway_456789_charge_enabled", "toggle")) assert gw._published == [("set_charge_enable", {"enable": False, "serial": "CE123456789"})] @@ -2154,7 +2154,7 @@ def test_discharge_enabled_turn_off(self): def test_discharge_enabled_toggle(self): """Toggling discharge_enabled flips based on current state from get_state_wrapper.""" gw = self._make_gateway() - # currently off → toggle → on (get_state_wrapper returns False by default) + # currently off -> toggle -> on (get_state_wrapper returns False by default) self._run(gw.switch_event("switch.predbat_gateway_456789_discharge_enabled", "toggle")) assert gw._published == [("set_discharge_enable", {"enable": True, "serial": "CE123456789"})] @@ -2381,49 +2381,49 @@ def _get_payload_with_plan(self, rows, extra_states=None): return json.loads(raw_bytes.decode()) def test_timeline_charging_maps_to_1(self): - """Charging state → timeline code 1.""" + """Charging state -> timeline code 1.""" rows = self._make_rows([{"state": "Chrg", "soc_percent": 60}]) payload = self._get_payload_with_plan(rows) assert payload["timeline"][0] == 1 def test_timeline_freeze_charging_maps_to_1(self): - """Freeze charging state → timeline code 1.""" + """Freeze charging state -> timeline code 1.""" rows = self._make_rows([{"state": "FrzChrg", "soc_percent": 60}]) payload = self._get_payload_with_plan(rows) assert payload["timeline"][0] == 1 def test_timeline_hold_charging_maps_to_1(self): - """Hold charging state → timeline code 1.""" + """Hold charging state -> timeline code 1.""" rows = self._make_rows([{"state": "HoldChrg", "soc_percent": 60}]) payload = self._get_payload_with_plan(rows) assert payload["timeline"][0] == 1 def test_timeline_discharging_maps_to_2(self): - """Discharging state → timeline code 2.""" + """Discharging state -> timeline code 2.""" rows = self._make_rows([{"state": "Exp", "soc_percent": 40}]) payload = self._get_payload_with_plan(rows) assert payload["timeline"][0] == 2 def test_timeline_freeze_discharging_maps_to_2(self): - """Freeze discharging state → timeline code 2.""" + """Freeze discharging state -> timeline code 2.""" rows = self._make_rows([{"state": "FrzExp", "soc_percent": 40}]) payload = self._get_payload_with_plan(rows) assert payload["timeline"][0] == 2 def test_timeline_hold_discharging_maps_to_2(self): - """Hold export/discharge state → timeline code 2.""" + """Hold export/discharge state -> timeline code 2.""" rows = self._make_rows([{"state": "HoldExp", "soc_percent": 40}]) payload = self._get_payload_with_plan(rows) assert payload["timeline"][0] == 2 def test_timeline_solar_maps_to_3(self): - """Unknown state with pv_forecast > 0.1 → timeline code 3 (solar).""" + """Unknown state with pv_forecast > 0.1 -> timeline code 3 (solar).""" rows = self._make_rows([{"state": "Idle", "soc_percent": 50, "pv_forecast": 0.5}]) payload = self._get_payload_with_plan(rows) assert payload["timeline"][0] == 3 def test_timeline_grid_import_maps_to_4(self): - """Unknown state with pv_forecast <= 0.1 → timeline code 4 (grid import).""" + """Unknown state with pv_forecast <= 0.1 -> timeline code 4 (grid import).""" rows = self._make_rows([{"state": "Idle", "soc_percent": 50, "pv_forecast": 0.0}]) payload = self._get_payload_with_plan(rows) assert payload["timeline"][0] == 4 @@ -2740,13 +2740,13 @@ def test_southern_hemisphere_standard_offset(self): tz = _pytz.timezone("Australia/Sydney") # Manually compute what the fallback would produce using just January (wrong) off_jan = tz.utcoffset(datetime.datetime(2024, 1, 15, 12, 0, 0)).total_seconds() / 60 - # January is DST in Sydney: UTC+11 → posix_total = -660 → hours = -11 (wrong) + # January is DST in Sydney: UTC+11 -> posix_total = -660 -> hours = -11 (wrong) # The correct standard offset is UTC+10 (July) off_jul = tz.utcoffset(datetime.datetime(2024, 7, 15, 12, 0, 0)).total_seconds() / 60 assert off_jan > off_jul, "Precondition: Sydney has higher offset in Jan (DST) than Jul (standard)" # The actual conversion should use the smaller offset (July = standard time) result = self._convert("Australia/Sydney") - # Standard AEST is UTC+10; POSIX sign flips → "-10" must appear # cspell:disable-line + # Standard AEST is UTC+10; POSIX sign flips -> "-10" must appear # cspell:disable-line assert "-10" in result, f"Expected standard offset -10 in POSIX string, got {result!r}" # cspell:disable-line def test_asia_kolkata_half_hour_offset(self): @@ -2788,7 +2788,7 @@ def test_negative_fractional_offset(self): assert "3:30" in result, f"Expected '3:30' in POSIX string, got {result!r}" # cspell:disable-line def test_fallback_northern_hemisphere(self): - """Fallback path (TZif file bypassed): Europe/London standard time is GMT (UTC+0) → 'GMT0'.""" + """Fallback path (TZif file bypassed): Europe/London standard time is GMT (UTC+0) -> 'GMT0'.""" from unittest.mock import patch from gateway import GatewayMQTT @@ -2808,7 +2808,7 @@ def test_fallback_southern_hemisphere(self): assert "-11" not in result, f"DST offset -11 must not appear in fallback result, got {result!r}" # cspell:disable-line def test_fallback_negative_fractional_offset(self): - """Fallback path divmod fix: America/St_Johns standard time is NST (UTC-3:30) → '3:30'.""" + """Fallback path divmod fix: America/St_Johns standard time is NST (UTC-3:30) -> '3:30'.""" from unittest.mock import patch from gateway import GatewayMQTT @@ -3179,7 +3179,7 @@ def _make_gateway(self, read_only=False, alive=True, auto_configured=True): gw._suffix_to_serial = {} gw._inverter_reset_done = set() gw._mqtt_connected = alive - gw._gateway_online = False # broker-connected but LWT-offline → is_alive() True when _mqtt_connected + gw._gateway_online = False # broker-connected but LWT-offline -> is_alive() True when _mqtt_connected gw._last_telemetry_time = 0 gw._auto_configured = auto_configured gw._published = [] @@ -3651,7 +3651,7 @@ def test_set_charge_slot_start_raw_payload(self): import json gw = self._make_gateway() - # 02:00:00 → HHMM 200, matching the expected {"start":200} in the hub spec + # 02:00:00 -> HHMM 200, matching the expected {"start":200} in the hub spec self._run(gw.select_event("select.predbat_gateway_30g499_charge_slot1_start", "02:00:00")) assert gw._raw_published, "No payload was published — serial lookup may have failed" @@ -3673,7 +3673,7 @@ def test_set_charge_slot_start_raw_payload(self): class TestEvTelemetry: - """Tests for GatewayMQTT._inject_ev_entities() — EvCharger telemetry → entities.""" + """Tests for GatewayMQTT._inject_ev_entities() — EvCharger telemetry -> entities.""" def _make_gateway(self, battery_size=100): from gateway import GatewayMQTT @@ -3683,7 +3683,7 @@ def _make_gateway(self, battery_size=100): gw.base = MagicMock() gw.log = MagicMock() gw.prefix = "predbat" - gw._dashboard_calls = {} # entity_id → (state, attributes) + gw._dashboard_calls = {} # entity_id -> (state, attributes) def capture_dashboard(entity_id, state=None, attributes=None, app=None): gw._dashboard_calls[entity_id] = (state, attributes) @@ -3717,11 +3717,11 @@ def test_ev_entities_published(self): base = "predbat_gateway_ev" assert gw._dashboard_calls[f"binary_sensor.{base}_online"][0] is True - assert gw._dashboard_calls[f"binary_sensor.{base}_connected"][0] is True # status="Charging" → car connected + assert gw._dashboard_calls[f"binary_sensor.{base}_connected"][0] is True # status="Charging" -> car connected assert gw._dashboard_calls[f"binary_sensor.{base}_session_active"][0] is True assert gw._dashboard_calls[f"sensor.{base}_status"][0] == "Charging" assert gw._dashboard_calls[f"sensor.{base}_power"][0] == 7200 - # Wh → kWh + # Wh -> kWh assert approx_equal(gw._dashboard_calls[f"sensor.{base}_session_energy"][0], 12.4) assert gw._dashboard_calls[f"sensor.{base}_current_limit"][0] == 16 assert gw._dashboard_calls[f"sensor.{base}_soc"][0] == 55 @@ -3743,7 +3743,7 @@ def test_ev_entity_attributes_from_table(self): def test_not_reported_fields_skipped(self): """Zero/empty 'not reported' fields are not published, except soc which falls back to session energy.""" gw = self._make_gateway(battery_size=100) - # session_energy_wh=12400 → 12.4 kWh; battery_size=100 → fallback soc = 12.4% + # session_energy_wh=12400 -> 12.4 kWh; battery_size=100 -> fallback soc = 12.4% status = self._status_with_ev(soc_percent=0, voltage_v=0, max_current_a=0, current_limit_a=0, eco_mode="", status="") gw._inject_ev_entities(status) @@ -3757,7 +3757,7 @@ def test_not_reported_fields_skipped(self): assert f"sensor.{base}_status" not in gw._dashboard_calls # Always-published fields remain assert f"binary_sensor.{base}_online" in gw._dashboard_calls - assert gw._dashboard_calls[f"binary_sensor.{base}_connected"][0] is False # status="" → no car + assert gw._dashboard_calls[f"binary_sensor.{base}_connected"][0] is False # status="" -> no car assert f"sensor.{base}_power" in gw._dashboard_calls # Charge rate falls back to 7.4 kW when capability is not reported assert approx_equal(gw._dashboard_calls[f"sensor.{base}_charge_rate"][0], 7.4) @@ -3765,7 +3765,7 @@ def test_not_reported_fields_skipped(self): def test_soc_fallback_uses_battery_size(self): """When soc is not reported, the fallback is session_energy / battery_size * 100.""" gw = self._make_gateway(battery_size=50) - # session_energy_wh=12400 → 12.4 kWh; battery_size=50 → 12.4/50*100 = 24.8% + # session_energy_wh=12400 -> 12.4 kWh; battery_size=50 -> 12.4/50*100 = 24.8% gw._inject_ev_entities(self._status_with_ev(soc_percent=0)) assert approx_equal(gw._dashboard_calls["sensor.predbat_gateway_ev_soc"][0], 24.8) diff --git a/apps/predbat/tests/test_ge_cloud.py b/apps/predbat/tests/test_ge_cloud.py index 47a7acfe9..e56b0536d 100644 --- a/apps/predbat/tests/test_ge_cloud.py +++ b/apps/predbat/tests/test_ge_cloud.py @@ -264,13 +264,13 @@ def test_ge_cloud(my_predbat=None): try: result = test_func(my_predbat) if result: - print(f"✗ FAILED: {key}") + print(f"FAIL: FAILED: {key}") failed += 1 else: - print(f"✓ PASSED: {key}") + print(f"PASS: PASSED: {key}") passed += 1 except Exception as e: - print(f"✗ EXCEPTION in {key}: {e}") + print(f"FAIL: EXCEPTION in {key}: {e}") import traceback traceback.print_exc() diff --git a/apps/predbat/tests/test_hahistory.py b/apps/predbat/tests/test_hahistory.py index 1b80aaa7f..58e7e2847 100644 --- a/apps/predbat/tests/test_hahistory.py +++ b/apps/predbat/tests/test_hahistory.py @@ -117,7 +117,7 @@ def test_hahistory_initialize(my_predbat=None): print("ERROR: history_entities should be empty after initialisation") failed += 1 else: - print("✓ history_entities initialised correctly") + print("PASS: history_entities initialised correctly") if not isinstance(ha_history.history_data, dict): print("ERROR: history_data should be a dict") @@ -126,7 +126,7 @@ def test_hahistory_initialize(my_predbat=None): print("ERROR: history_data should be empty after initialisation") failed += 1 else: - print("✓ history_data initialised correctly") + print("PASS: history_data initialised correctly") return failed @@ -146,7 +146,7 @@ def test_hahistory_add_entity(my_predbat=None): print("ERROR: Failed to add new entity with 30 days") failed += 1 else: - print("✓ Added new entity with 30 days") + print("PASS: Added new entity with 30 days") # Test updating entity with fewer days (should not update) ha_history.add_entity("sensor.battery", 7) @@ -154,7 +154,7 @@ def test_hahistory_add_entity(my_predbat=None): print("ERROR: Should not update entity to fewer days") failed += 1 else: - print("✓ Correctly kept maximum days (30)") + print("PASS: Correctly kept maximum days (30)") # Test updating entity with more days (should update) ha_history.add_entity("sensor.battery", 60) @@ -162,7 +162,7 @@ def test_hahistory_add_entity(my_predbat=None): print("ERROR: Failed to update entity to more days") failed += 1 else: - print("✓ Updated entity to more days (60)") + print("PASS: Updated entity to more days (60)") # Test adding multiple entities ha_history.add_entity("sensor.solar", 14) @@ -171,7 +171,7 @@ def test_hahistory_add_entity(my_predbat=None): print("ERROR: Should have 3 entities tracked") failed += 1 else: - print("✓ Multiple entities tracked correctly") + print("PASS: Multiple entities tracked correctly") return failed @@ -192,7 +192,7 @@ def test_hahistory_get_history_no_interface(my_predbat=None): print("ERROR: Should return None when no HAInterface") failed += 1 else: - print("✓ Returned None when no HAInterface") + print("PASS: Returned None when no HAInterface") # Check for error log error_found = any("No HAInterface available" in msg for msg in mock_base.log_messages) @@ -200,7 +200,7 @@ def test_hahistory_get_history_no_interface(my_predbat=None): print("ERROR: Should log error when no HAInterface") failed += 1 else: - print("✓ Logged error when no HAInterface") + print("PASS: Logged error when no HAInterface") return failed @@ -239,7 +239,7 @@ def test_hahistory_get_history_fetch_and_cache(my_predbat=None): print(f"ERROR: Expected {len(mock_history)} entries, got {len(result[0])}") failed += 1 else: - print(f"✓ Fetched history with {len(result[0])} entries") + print(f"PASS: Fetched history with {len(result[0])} entries") # Verify entity was tracked if entity_id not in ha_history.history_entities: @@ -249,14 +249,14 @@ def test_hahistory_get_history_fetch_and_cache(my_predbat=None): print("ERROR: Entity should be tracked with 30 days") failed += 1 else: - print("✓ Entity tracked correctly") + print("PASS: Entity tracked correctly") # Verify data was cached if entity_id not in ha_history.history_data: print("ERROR: History should be cached") failed += 1 else: - print("✓ History cached correctly") + print("PASS: History cached correctly") # Test 2: Get from cache (should not call HAInterface again) initial_call_count = len(mock_ha.get_history_calls) @@ -266,7 +266,7 @@ def test_hahistory_get_history_fetch_and_cache(my_predbat=None): print("ERROR: Should use cache, not call HAInterface again") failed += 1 else: - print("✓ Used cache instead of fetching again") + print("PASS: Used cache instead of fetching again") # Test 3: Request more days (should fetch again) result3 = ha_history.get_history(entity_id, days=60, tracked=True) @@ -274,14 +274,14 @@ def test_hahistory_get_history_fetch_and_cache(my_predbat=None): print("ERROR: Should fetch when requesting more days") failed += 1 else: - print("✓ Fetched when requesting more days") + print("PASS: Fetched when requesting more days") # Verify entity tracking was updated if ha_history.history_entities[entity_id] != 60: print("ERROR: Entity tracking should be updated to 60 days") failed += 1 else: - print("✓ Entity tracking updated to 60 days") + print("PASS: Entity tracking updated to 60 days") # Test 4: Request more days again - cache should have them populated result4 = ha_history.get_history(entity_id, days=60, tracked=True) @@ -289,14 +289,14 @@ def test_hahistory_get_history_fetch_and_cache(my_predbat=None): print("ERROR: Cache did not correctly return 60 days") failed += 1 else: - print("✓ Cache correctly populated with longer history") + print("PASS: Cache correctly populated with longer history") sorted_result4 = sorted(result4[0], key=lambda x: x["last_updated"]) if sorted_result4 != result4[0]: print("ERROR: Cache population did not correctly order entries when adding longer history") failed += 1 else: - print("✓ Cache order correct after adding longer history") + print("PASS: Cache order correct after adding longer history") return failed @@ -325,21 +325,21 @@ def test_hahistory_get_history_untracked(my_predbat=None): print("ERROR: Should return history data") failed += 1 else: - print("✓ Fetched untracked history") + print("PASS: Fetched untracked history") # Verify entity was NOT tracked if entity_id in ha_history.history_entities: print("ERROR: Entity should not be tracked when tracked=False") failed += 1 else: - print("✓ Entity not tracked correctly") + print("PASS: Entity not tracked correctly") # Verify data was NOT cached if entity_id in ha_history.history_data: print("ERROR: History should not be cached when tracked=False") failed += 1 else: - print("✓ History not cached correctly") + print("PASS: History not cached correctly") return failed @@ -376,14 +376,14 @@ def test_hahistory_expand_from_7_to_30_days(my_predbat=None): print(f"ERROR: Expected {length_7_days} entries for 7 days, got {len(result_7[0])}") failed += 1 else: - print(f"✓ Fetched and cached 7 days ({len(result_7[0])} entries)") + print(f"PASS: Fetched and cached 7 days ({len(result_7[0])} entries)") # Verify entity is tracked with 7 days if ha_history.history_entities.get(entity_id) != 7: print(f"ERROR: Entity should be tracked with 7 days, but has {ha_history.history_entities.get(entity_id)}") failed += 1 else: - print("✓ Entity tracked with 7 days") + print("PASS: Entity tracked with 7 days") # Step 2: Now request 30 days (more than cached) print("\n Step 2: Requesting 30 days (more than cached 7 days)...") @@ -395,7 +395,7 @@ def test_hahistory_expand_from_7_to_30_days(my_predbat=None): print("ERROR: Should fetch from HAInterface when requesting more days than cached") failed += 1 else: - print("✓ Correctly triggered new fetch from HAInterface") + print("PASS: Correctly triggered new fetch from HAInterface") # Verify we got the FULL 30 days, not just 7 if result_30 is None: @@ -406,14 +406,14 @@ def test_hahistory_expand_from_7_to_30_days(my_predbat=None): print(f" This suggests only {len(result_30[0]) / FIVE_MINUTE_ENTRIES_PER_DAY:.1f} days were returned") failed += 1 else: - print(f"✓ Correctly returned FULL 30 days of history ({len(result_30[0])} entries)") + print(f"PASS: Correctly returned FULL 30 days of history ({len(result_30[0])} entries)") # Verify entity tracking was updated to 30 days if ha_history.history_entities.get(entity_id) != 30: print(f"ERROR: Entity should be tracked with 30 days, but has {ha_history.history_entities.get(entity_id)}") failed += 1 else: - print("✓ Entity tracking updated to 30 days") + print("PASS: Entity tracking updated to 30 days") # Verify the last fetch call requested 30 days last_call = mock_ha.get_history_calls[-1] @@ -421,7 +421,7 @@ def test_hahistory_expand_from_7_to_30_days(my_predbat=None): print(f"ERROR: Last fetch should have requested 30 days, but requested {last_call['days']}") failed += 1 else: - print("✓ Fetch correctly requested 30 days from backend") + print("PASS: Fetch correctly requested 30 days from backend") # Step 3: Request 30 days again - should use cache now print("\n Step 3: Requesting 30 days again (should use cache)...") @@ -432,13 +432,13 @@ def test_hahistory_expand_from_7_to_30_days(my_predbat=None): print("ERROR: Should use cache when requesting same or fewer days") failed += 1 else: - print("✓ Correctly used cache for subsequent request") + print("PASS: Correctly used cache for subsequent request") if len(result_30_again[0]) != length_30_days: print(f"ERROR: Cached result should still have {length_30_days} entries, got {len(result_30_again[0])}") failed += 1 else: - print("✓ Cache correctly returns full 30 days") + print("PASS: Cache correctly returns full 30 days") return failed @@ -516,7 +516,7 @@ def test_hahistory_update_entity_filter_attributes(my_predbat=None): failed += 1 if failed == 0: - print("✓ Attributes filtered correctly") + print("PASS: Attributes filtered correctly") return failed @@ -547,7 +547,7 @@ def test_hahistory_update_entity_merge_new(my_predbat=None): print(f"ERROR: Should have 3 initial entries, got {initial_count}") failed += 1 else: - print("✓ Initial history loaded correctly") + print("PASS: Initial history loaded correctly") # New history with overlapping and new entries new_history = [ @@ -563,7 +563,7 @@ def test_hahistory_update_entity_merge_new(my_predbat=None): print(f"ERROR: Should have 5 entries after merge (3 old + 2 new), got {final_count}") failed += 1 else: - print("✓ New entries merged correctly") + print("PASS: New entries merged correctly") # Verify ordering (oldest to newest) - numeric states come back as floats from the columnar store states = [entry["state"] for entry in ha_history.history_data[entity_id]] @@ -572,7 +572,7 @@ def test_hahistory_update_entity_merge_new(my_predbat=None): print(f"ERROR: Expected states {expected_states}, got {states}") failed += 1 else: - print("✓ History maintained correct order") + print("PASS: History maintained correct order") return failed @@ -613,14 +613,14 @@ def test_hahistory_prune_history(my_predbat=None): break if failed == 0: - print("✓ All entries within 30-day window") + print("PASS: All entries within 30-day window") # Verify some entries were removed if final_count >= initial_count: print("ERROR: Expected entries to be removed") failed += 1 else: - print(f"✓ Pruned {initial_count - final_count} old entries") + print(f"PASS: Pruned {initial_count - final_count} old entries") return failed @@ -643,7 +643,7 @@ def test_hahistory_prune_empty_history(my_predbat=None): # Should not crash with empty history try: ha_history.prune_history(now) - print("✓ Handled empty history without error") + print("PASS: Handled empty history without error") except Exception as e: print(f"ERROR: Exception with empty history: {e}") failed += 1 @@ -684,21 +684,21 @@ async def run_test(): print("ERROR: Should log startup message") return 1 else: - print("✓ Logged startup message") + print("PASS: Logged startup message") # Verify history was fetched for tracked entity if len(mock_ha.get_history_calls) == 0: print("ERROR: Should fetch history on first run") return 1 else: - print(f"✓ Fetched history for {len(mock_ha.get_history_calls)} entity(ies)") + print(f"PASS: Fetched history for {len(mock_ha.get_history_calls)} entity(ies)") # Verify history was cached if entity_id not in ha_history.history_data: print("ERROR: History should be cached") return 1 else: - print("✓ History cached correctly") + print("PASS: History cached correctly") return 0 @@ -729,7 +729,7 @@ async def run_test(): print("ERROR: Should log error when no HAInterface") return 1 else: - print("✓ Returned False and logged error") + print("PASS: Returned False and logged error") return 0 @@ -776,7 +776,7 @@ async def run_test(): print("ERROR: Should fetch history at 2-minute interval") return 1 else: - print("✓ Fetched history at 2-minute interval") + print("PASS: Fetched history at 2-minute interval") # Verify incremental fetch (with from_time) last_call = mock_ha.get_history_calls[-1] @@ -784,7 +784,7 @@ async def run_test(): print("ERROR: Should use from_time for incremental fetch") return 1 else: - print("✓ Used incremental fetch with from_time") + print("PASS: Used incremental fetch with from_time") return 0 @@ -829,7 +829,7 @@ async def run_test(): print("ERROR: Should log pruning message") return 1 else: - print("✓ Logged pruning message") + print("PASS: Logged pruning message") # Verify entries were pruned final_count = len(ha_history.history_data[entity_id]) @@ -837,7 +837,7 @@ async def run_test(): print("ERROR: Expected entries to be pruned") return 1 else: - print(f"✓ Pruned {initial_count - final_count} entries") + print(f"PASS: Pruned {initial_count - final_count} entries") return 0 @@ -875,7 +875,7 @@ async def run_test(): print("ERROR: Should not fetch history at 60 seconds") return 1 else: - print("✓ Correctly skipped update at non-trigger time") + print("PASS: Correctly skipped update at non-trigger time") return 0 @@ -904,7 +904,7 @@ def test_entity_history_store(my_predbat=None): print(f"ERROR: Expected 5 records in store, got {len(store)}") failed += 1 else: - print("✓ Store holds all records") + print("PASS: Store holds all records") # Test 2: round-trip states - numeric become floats, non-numeric preserved exactly output = store.to_records() @@ -914,7 +914,7 @@ def test_entity_history_store(my_predbat=None): print(f"ERROR: Expected states {expected}, got {states}") failed += 1 else: - print("✓ States round-trip correctly (numeric as float, non-numeric preserved)") + print("PASS: States round-trip correctly (numeric as float, non-numeric preserved)") # Test 3: round-trip timestamps parse back to the same instant times_in = [str2time(entry["last_updated"]) for entry in records] @@ -923,7 +923,7 @@ def test_entity_history_store(my_predbat=None): print(f"ERROR: Timestamps did not round-trip, in {times_in} out {times_out}") failed += 1 else: - print("✓ Timestamps round-trip to the same instant") + print("PASS: Timestamps round-trip to the same instant") # Test 4: attribute exceptions are preserved, shared attributes for the rest if output[2]["attributes"] != {"special": True}: @@ -933,7 +933,7 @@ def test_entity_history_store(my_predbat=None): print(f"ERROR: Expected empty shared attributes, got {output[0]['attributes']} and {output[4]['attributes']}") failed += 1 else: - print("✓ Attribute exceptions preserved, shared attributes returned for the rest") + print("PASS: Attribute exceptions preserved, shared attributes returned for the rest") # Test 5: mutation of materialised records does not corrupt the store output[0]["state"] = "corrupted" @@ -943,14 +943,14 @@ def test_entity_history_store(my_predbat=None): print(f"ERROR: Store was corrupted by mutating materialised records: {fresh[0]}") failed += 1 else: - print("✓ Materialised records are safe to mutate") + print("PASS: Materialised records are safe to mutate") # Test 6: indexing (including negative) and iteration match to_records if store[0]["state"] != 10.5 or store[-1]["state"] != 30.0 or [entry["state"] for entry in store] != expected: print(f"ERROR: Indexing/iteration mismatch: first {store[0]} last {store[-1]}") failed += 1 else: - print("✓ Indexing and iteration materialise records correctly") + print("PASS: Indexing and iteration materialise records correctly") # Test 7: prune drops older records and their exceptions store.prune((base_time + timedelta(minutes=12)).timestamp()) @@ -965,7 +965,7 @@ def test_entity_history_store(my_predbat=None): print(f"ERROR: Prune left stale attribute exceptions: {store.attribute_exceptions}") failed += 1 else: - print("✓ Prune drops old records and stale exceptions") + print("PASS: Prune drops old records and stale exceptions") # Test 8: records without a valid last_updated are not stored bad_store = EntityHistory.from_records([{"state": "1"}, {"state": "2", "last_updated": "junk"}, {"state": "3", "last_updated": base_time.isoformat()}]) @@ -973,7 +973,7 @@ def test_entity_history_store(my_predbat=None): print(f"ERROR: Expected 1 valid record stored, got {len(bad_store)}") failed += 1 else: - print("✓ Records without a valid last_updated are dropped") + print("PASS: Records without a valid last_updated are dropped") return failed @@ -1012,9 +1012,9 @@ def run_hahistory_tests(my_predbat): print("\n" + "=" * 80) if failed == 0: - print("✅ All HAHistory tests passed!") + print("PASS: All HAHistory tests passed!") else: - print(f"❌ {failed} HAHistory test(s) failed") + print(f"ERROR: {failed} HAHistory test(s) failed") print("=" * 80 + "\n") return failed diff --git a/apps/predbat/tests/test_hainterface_api.py b/apps/predbat/tests/test_hainterface_api.py index 65cf73496..e50aa711d 100644 --- a/apps/predbat/tests/test_hainterface_api.py +++ b/apps/predbat/tests/test_hainterface_api.py @@ -42,14 +42,14 @@ def test_hainterface_api_call_get(my_predbat=None): print("ERROR: Authorization header missing") failed += 1 else: - print("✓ GET request made correctly") + print("PASS: GET request made correctly") # Verify result if result != {"result": "success"}: print(f"ERROR: Wrong result: {result}") failed += 1 else: - print("✓ Result returned correctly") + print("PASS: Result returned correctly") return failed @@ -80,14 +80,14 @@ def test_hainterface_api_call_post(my_predbat=None): print("ERROR: Wrong JSON data") failed += 1 else: - print("✓ POST request made correctly") + print("PASS: POST request made correctly") # Verify result if result != {"status": "ok"}: print(f"ERROR: Wrong result: {result}") failed += 1 else: - print("✓ Result returned correctly") + print("PASS: Result returned correctly") return failed @@ -106,7 +106,7 @@ def test_hainterface_api_call_no_key(my_predbat=None): print(f"ERROR: Should return None, got {result}") failed += 1 else: - print("✓ Returned None when no API key") + print("PASS: Returned None when no API key") return failed @@ -139,7 +139,7 @@ def test_hainterface_api_call_supervisor(my_predbat=None): print("ERROR: Supervisor token not used") failed += 1 else: - print("✓ Supervisor endpoint called correctly") + print("PASS: Supervisor endpoint called correctly") return failed @@ -166,14 +166,14 @@ def test_hainterface_api_call_json_decode_error(my_predbat=None): print(f"ERROR: Should return None on JSON error, got {result}") failed += 1 else: - print("✓ Returned None on JSON decode error") + print("PASS: Returned None on JSON decode error") # Verify error count incremented if ha_interface.api_errors != 1: print(f"ERROR: api_errors should be 1, got {ha_interface.api_errors}") failed += 1 else: - print("✓ api_errors incremented") + print("PASS: api_errors incremented") return failed @@ -197,14 +197,14 @@ def test_hainterface_api_call_timeout(my_predbat=None): print(f"ERROR: Should return None on timeout, got {result}") failed += 1 else: - print("✓ Returned None on timeout") + print("PASS: Returned None on timeout") # Verify error count incremented if ha_interface.api_errors != 1: print(f"ERROR: api_errors should be 1, got {ha_interface.api_errors}") failed += 1 else: - print("✓ api_errors incremented") + print("PASS: api_errors incremented") return failed @@ -228,14 +228,14 @@ def test_hainterface_api_call_read_timeout(my_predbat=None): print(f"ERROR: Should return None on read timeout, got {result}") failed += 1 else: - print("✓ Returned None on ReadTimeout") + print("PASS: Returned None on ReadTimeout") # Verify error count incremented if ha_interface.api_errors != 1: print(f"ERROR: api_errors should be 1, got {ha_interface.api_errors}") failed += 1 else: - print("✓ api_errors incremented") + print("PASS: api_errors incremented") return failed @@ -273,7 +273,7 @@ def tracked_log(msg): print("ERROR: Warning should be suppressed in silent mode") failed += 1 else: - print("✓ Warning suppressed in silent mode") + print("PASS: Warning suppressed in silent mode") return failed @@ -303,7 +303,7 @@ def mock_fatal_error(): print("ERROR: fatal_error_occurred should be called at 10 errors") failed += 1 else: - print("✓ fatal_error_occurred called at error limit") + print("PASS: fatal_error_occurred called at error limit") return failed @@ -327,7 +327,7 @@ def test_hainterface_api_call_error_reset(my_predbat=None): print(f"ERROR: api_errors should be reset to 0, got {ha_interface.api_errors}") failed += 1 else: - print("✓ api_errors reset on success") + print("PASS: api_errors reset on success") return failed @@ -367,7 +367,7 @@ def test_hainterface_initialize_addon_check(my_predbat=None): print(f"ERROR: Slug should be 'predbat_addon', got {ha_interface.slug}") failed += 1 else: - print("✓ Addon slug detected correctly") + print("PASS: Addon slug detected correctly") return failed @@ -408,7 +408,7 @@ def test_hainterface_initialize_no_addon(my_predbat=None): print(f"ERROR: Slug should be None, got {ha_interface.slug}") failed += 1 else: - print("✓ Missing addon handled gracefully") + print("PASS: Missing addon handled gracefully") return failed @@ -444,7 +444,7 @@ def test_hainterface_get_history_basic(my_predbat=None): print("ERROR: API should be called") failed += 1 else: - print("✓ API called") + print("PASS: API called") # Verify result structure - get_history returns the list directly if not isinstance(result, list): @@ -457,7 +457,7 @@ def test_hainterface_get_history_basic(my_predbat=None): print(f"ERROR: History array should have 10 items, got {len(result[0])}") failed += 1 else: - print("✓ History data returned correctly") + print("PASS: History data returned correctly") return failed @@ -482,7 +482,7 @@ def test_hainterface_get_history_no_key(my_predbat=None): print(f"ERROR: Should return empty list from DB, got {result}") failed += 1 else: - print("✓ Used database when no API key") + print("PASS: Used database when no API key") return failed @@ -505,7 +505,7 @@ def test_hainterface_get_history_api_error(my_predbat=None): print(f"ERROR: Should return None on API error, got {result}") failed += 1 else: - print("✓ Returned None on API error") + print("PASS: Returned None on API error") return failed @@ -539,7 +539,7 @@ def test_hainterface_get_history_from_time(my_predbat=None): print(f"ERROR: from_time {expected_time_str} not in URL: {url}") failed += 1 else: - print("✓ from_time parameter used correctly") + print("PASS: from_time parameter used correctly") return failed @@ -570,9 +570,9 @@ def run_hainterface_api_tests(my_predbat): print("\n" + "=" * 80) if failed == 0: - print("✅ All HAInterface API tests passed!") + print("PASS: All HAInterface API tests passed!") else: - print(f"❌ {failed} HAInterface API test(s) failed") + print(f"ERROR: {failed} HAInterface API test(s) failed") print("=" * 80) return failed diff --git a/apps/predbat/tests/test_hainterface_lifecycle.py b/apps/predbat/tests/test_hainterface_lifecycle.py index cc406d574..bd3e0769f 100644 --- a/apps/predbat/tests/test_hainterface_lifecycle.py +++ b/apps/predbat/tests/test_hainterface_lifecycle.py @@ -46,7 +46,7 @@ def test_hainterface_initialize_ha_only(my_predbat=None): print("ERROR: websocket_active should be False initially") failed += 1 else: - print("✓ Initialized with HA only correctly") + print("PASS: Initialized with HA only correctly") return failed @@ -76,13 +76,13 @@ def test_hainterface_initialize_db_primary(my_predbat=None): print("ERROR: db_primary should be True") failed += 1 else: - print("✓ Initialized with DB primary correctly") + print("PASS: Initialized with DB primary correctly") if not any("SQL Lite database as primary" in log for log in mock_base.log_messages): print("ERROR: Should log DB primary mode") failed += 1 else: - print("✓ DB primary mode logged") + print("PASS: DB primary mode logged") return failed @@ -105,7 +105,7 @@ def test_hainterface_initialize_no_key_no_db(my_predbat=None): print("ERROR: Should raise ValueError") failed += 1 except ValueError: - print("✓ ValueError raised correctly") + print("PASS: ValueError raised correctly") return failed @@ -132,13 +132,13 @@ def test_hainterface_initialize_api_check_failed(my_predbat=None): print("ERROR: Should raise ValueError") failed += 1 except ValueError: - print("✓ ValueError raised on API check failure") + print("PASS: ValueError raised on API check failure") if not any("Unable to connect" in log for log in mock_base.log_messages): print("ERROR: Should log connection failure") failed += 1 else: - print("✓ Connection failure logged") + print("PASS: Connection failure logged") return failed @@ -167,7 +167,7 @@ def test_hainterface_initialize_db_mirror(my_predbat=None): print("ERROR: db_mirror_ha should be True") failed += 1 else: - print("✓ DB mirroring enabled correctly") + print("PASS: DB mirroring enabled correctly") return failed @@ -185,7 +185,7 @@ def test_hainterface_is_alive_not_started(my_predbat=None): print("ERROR: Should return False when not started") failed += 1 else: - print("✓ Returns False when not started") + print("PASS: Returns False when not started") return failed @@ -204,7 +204,7 @@ def test_hainterface_is_alive_no_websocket(my_predbat=None): print("ERROR: Should return False with ha_key but no websocket") failed += 1 else: - print("✓ Returns False without websocket") + print("PASS: Returns False without websocket") return failed @@ -222,7 +222,7 @@ def test_hainterface_is_alive_websocket_active(my_predbat=None): print("ERROR: Should return True with websocket active") failed += 1 else: - print("✓ Returns True with websocket active") + print("PASS: Returns True with websocket active") return failed @@ -240,7 +240,7 @@ def test_hainterface_is_alive_db_only(my_predbat=None): print("ERROR: Should return True in DB-only mode") failed += 1 else: - print("✓ Returns True in DB-only mode") + print("PASS: Returns True in DB-only mode") return failed @@ -262,7 +262,7 @@ def test_hainterface_wait_api_started_success(my_predbat=None): print("ERROR: Should return True when already started") failed += 1 else: - print("✓ Returns True when API started") + print("PASS: Returns True when API started") return failed @@ -294,13 +294,13 @@ def mock_sleep(seconds): print("ERROR: Should return False on timeout") failed += 1 else: - print("✓ Returns False on timeout") + print("PASS: Returns False on timeout") if not any("Failed to start" in log for log in mock_base.log_messages): print("ERROR: Should log timeout warning") failed += 1 else: - print("✓ Timeout warning logged") + print("PASS: Timeout warning logged") return failed @@ -335,7 +335,7 @@ def mock_get_side_effect(url, *args, **kwargs): print(f"ERROR: Expected slug 'predbat_addon', got '{slug}'") failed += 1 else: - print("✓ Slug retrieved correctly") + print("PASS: Slug retrieved correctly") return failed @@ -363,31 +363,31 @@ async def mock_socketloop(): print("ERROR: socketLoop should be called") failed += 1 else: - print("✓ socketLoop called") + print("PASS: socketLoop called") if ha_interface.websocket_active != True: print("ERROR: websocket_active should be True") failed += 1 else: - print("✓ websocket_active set to True") + print("PASS: websocket_active set to True") if ha_interface.api_started != False: print("ERROR: api_started should be False after exit") failed += 1 else: - print("✓ api_started set to False on exit") + print("PASS: api_started set to False on exit") if not any("Starting HA interface" in log for log in mock_base.log_messages): print("ERROR: Should log 'Starting HA interface'") failed += 1 else: - print("✓ Startup message logged") + print("PASS: Startup message logged") if not any("HA interface stopped" in log for log in mock_base.log_messages): print("ERROR: Should log 'HA interface stopped'") failed += 1 else: - print("✓ Stop message logged") + print("PASS: Stop message logged") return failed @@ -415,25 +415,25 @@ async def mock_sleep(delay): print("ERROR: Should log 'Starting Dummy HA interface'") failed += 1 else: - print("✓ Dummy startup message logged") + print("PASS: Dummy startup message logged") if ha_interface.api_started != False: print("ERROR: api_started should be False after exit") failed += 1 else: - print("✓ api_started set to False on exit") + print("PASS: api_started set to False on exit") if sleep_count[0] < 3: print(f"ERROR: Expected at least 3 sleep calls, got {sleep_count[0]}") failed += 1 else: - print(f"✓ Sleep called {sleep_count[0]} times") + print(f"PASS: Sleep called {sleep_count[0]} times") if not any("HA interface stopped" in log for log in mock_base.log_messages): print("ERROR: Should log 'HA interface stopped'") failed += 1 else: - print("✓ Stop message logged") + print("PASS: Stop message logged") return failed @@ -462,9 +462,9 @@ def run_hainterface_lifecycle_tests(my_predbat): print("\n" + "=" * 80) if failed == 0: - print("✅ All HAInterface lifecycle tests passed!") + print("PASS: All HAInterface lifecycle tests passed!") else: - print(f"❌ {failed} HAInterface lifecycle test(s) failed") + print(f"ERROR: {failed} HAInterface lifecycle test(s) failed") print("=" * 80) return failed diff --git a/apps/predbat/tests/test_hainterface_service.py b/apps/predbat/tests/test_hainterface_service.py index dde247d7e..fa219877c 100644 --- a/apps/predbat/tests/test_hainterface_service.py +++ b/apps/predbat/tests/test_hainterface_service.py @@ -49,7 +49,7 @@ def mock_call_service_websocket_command(domain, service, data): print(f"ERROR: Expected entity_id 'switch.test', got '{data.get('entity_id')}'") failed += 1 else: - print("✓ call_service_websocket_command called correctly") + print("PASS: call_service_websocket_command called correctly") ha_interface.call_service_websocket_command = original_method return failed @@ -89,7 +89,7 @@ async def mock_trigger_callback(data): print(f"ERROR: Expected value 42, got {data.get('service_data', {}).get('value')}") failed += 1 else: - print("✓ Loopback trigger_callback called correctly") + print("PASS: Loopback trigger_callback called correctly") return failed @@ -119,7 +119,7 @@ def background_processor(): # Verify command details if domain == "switch" and service == "turn_on" and service_data.get("entity_id") == "switch.test": - print("✓ Command queued correctly") + print("PASS: Command queued correctly") # Simulate socketLoop processing result_holder["success"] = True @@ -140,7 +140,7 @@ def background_processor(): failed += test_failed if test_failed == 0: - print("✓ Command processed successfully") + print("PASS: Command processed successfully") return failed @@ -193,8 +193,8 @@ def background_processor(): processor_thread.join(timeout=2.0) if result_data.get("checks_passed"): - print("✓ return_response removed from service_data") - print("✓ return_response flag set correctly") + print("PASS: return_response removed from service_data") + print("PASS: return_response flag set correctly") return result, 0 @@ -202,7 +202,7 @@ def background_processor(): failed += test_failed if test_failed == 0 and result == "test_value": - print("✓ Response returned correctly") + print("PASS: Response returned correctly") elif test_failed == 0: print(f"ERROR: Expected result 'test_value', got {result}") failed += 1 @@ -256,13 +256,13 @@ def background_processor(): print("ERROR: Should log warning on failure") failed += 1 else: - print("✓ Warning logged on failure") + print("PASS: Warning logged on failure") if result is not None: print(f"ERROR: Expected None result on failure, got {result}") failed += 1 else: - print("✓ Returned None on failure") + print("PASS: Returned None on failure") return failed @@ -300,13 +300,13 @@ async def test_command(): print("ERROR: Should log warning on timeout") failed += 1 else: - print("✓ Warning logged on timeout") + print("PASS: Warning logged on timeout") if result is not None: print(f"ERROR: Expected None result on timeout, got {result}") failed += 1 else: - print("✓ Returned None on timeout") + print("PASS: Returned None on timeout") return failed @@ -357,13 +357,13 @@ def background_processor(): print("ERROR: Should log warning on error") failed += 1 else: - print("✓ Warning logged on error") + print("PASS: Warning logged on error") if result is not None: print(f"ERROR: Expected None result on error, got {result}") failed += 1 else: - print("✓ Returned None on error") + print("PASS: Returned None on error") return failed @@ -425,21 +425,21 @@ def simulate_connection_drop(): print(f"ERROR: Caller blocked for {elapsed:.2f}s — should have returned promptly on connection_lost") failed += 1 else: - print(f"\u2713 Caller returned promptly in {elapsed:.3f}s (not the 2-minute timeout)") + print(f"PASS: Caller returned promptly in {elapsed:.3f}s (not the 2-minute timeout)") # Must log the 'connection_lost' failure warning if not any("Service call" in log and "failed" in log for log in mock_base.log_messages): print("ERROR: Should log warning that service call failed with connection_lost") failed += 1 else: - print("\u2713 Warning logged for connection_lost") + print("PASS: Warning logged for connection_lost") # Must return None (failure) if result is not None: print(f"ERROR: Expected None on connection_lost, got {result}") failed += 1 else: - print("\u2713 Returned None on connection_lost") + print("PASS: Returned None on connection_lost") return failed @@ -479,7 +479,7 @@ async def mock_trigger_callback(data): print(f"ERROR: Expected entity_id 'switch.test_switch'") failed += 1 else: - print("✓ CONFIG_ITEMS switch handled correctly") + print("PASS: CONFIG_ITEMS switch handled correctly") return failed @@ -518,7 +518,7 @@ async def mock_trigger_callback(data): print(f"ERROR: Expected value 42") failed += 1 else: - print("✓ CONFIG_ITEMS input_number handled correctly") + print("PASS: CONFIG_ITEMS input_number handled correctly") return failed @@ -557,7 +557,7 @@ async def mock_trigger_callback(data): print(f"ERROR: Expected option 'option2'") failed += 1 else: - print("✓ CONFIG_ITEMS select handled correctly") + print("PASS: CONFIG_ITEMS select handled correctly") return failed @@ -586,7 +586,7 @@ async def mock_trigger_callback(data): print(f"ERROR: Expected service 'turn_on'") failed += 1 else: - print("✓ Domain-based switch handled correctly") + print("PASS: Domain-based switch handled correctly") return failed @@ -618,7 +618,7 @@ async def mock_trigger_callback(data): print(f"ERROR: Expected value 50") failed += 1 else: - print("✓ Domain-based number handled correctly") + print("PASS: Domain-based number handled correctly") return failed @@ -650,7 +650,7 @@ async def mock_trigger_callback(data): print(f"ERROR: Expected option 'option1'") failed += 1 else: - print("✓ Domain-based select handled correctly") + print("PASS: Domain-based select handled correctly") return failed @@ -684,7 +684,7 @@ async def mock_trigger_callback(data): print("ERROR: Expected time '12:30:00'") failed += 1 else: - print("✓ Domain-based input_datetime time handled correctly") + print("PASS: Domain-based input_datetime time handled correctly") return failed @@ -722,7 +722,7 @@ async def mock_trigger_callback(data): print("ERROR: Expected datetime payload") failed += 1 else: - print("✓ input_datetime datetime payload handled correctly") + print("PASS: input_datetime datetime payload handled correctly") return failed @@ -760,7 +760,7 @@ async def mock_trigger_callback(data): print("ERROR: Expected date payload") failed += 1 else: - print("✓ input_datetime date payload handled correctly") + print("PASS: input_datetime date payload handled correctly") return failed @@ -794,7 +794,7 @@ async def mock_trigger_callback(data): print("ERROR: Expected value 'abc'") failed += 1 else: - print("✓ Domain-based input_text handled correctly") + print("PASS: Domain-based input_text handled correctly") return failed @@ -831,7 +831,7 @@ def mock_set_state(entity_id, state, attributes={}): print(f"ERROR: Expected state 123, got {state}") failed += 1 else: - print("✓ Sensor set_state called correctly") + print("PASS: Sensor set_state called correctly") ha_interface.set_state = original_set_state return failed @@ -877,7 +877,7 @@ async def mock_trigger_watch_list(entity_id, attributes, old_state, new_state): print(f"ERROR: Expected new_state 200") failed += 1 else: - print("✓ watch_list triggered on value change") + print("PASS: watch_list triggered on value change") return failed @@ -910,7 +910,7 @@ async def mock_trigger_watch_list(entity_id, attributes, old_state, new_state): print("ERROR: watch_list should not be triggered when value unchanged") failed += 1 else: - print("✓ watch_list not triggered when value unchanged") + print("PASS: watch_list not triggered when value unchanged") return failed @@ -946,9 +946,9 @@ def run_hainterface_service_tests(my_predbat): print("\n" + "=" * 80) if failed == 0: - print("✅ All HAInterface service tests passed!") + print("PASS: All HAInterface service tests passed!") else: - print(f"❌ {failed} HAInterface service test(s) failed") + print(f"ERROR: {failed} HAInterface service test(s) failed") print("=" * 80) return failed diff --git a/apps/predbat/tests/test_hainterface_state.py b/apps/predbat/tests/test_hainterface_state.py index a9633f8ab..1a39e439b 100644 --- a/apps/predbat/tests/test_hainterface_state.py +++ b/apps/predbat/tests/test_hainterface_state.py @@ -36,7 +36,7 @@ def test_hainterface_get_state_no_entity(my_predbat=None): print("ERROR: Should return full state_data dict") failed += 1 else: - print("✓ Returned full state_data dict") + print("PASS: Returned full state_data dict") return failed @@ -56,7 +56,7 @@ def test_hainterface_get_state_cached(my_predbat=None): print(f"ERROR: Expected '50', got '{result}'") failed += 1 else: - print("✓ Retrieved cached state value") + print("PASS: Retrieved cached state value") # Test with attribute result = ha_interface.get_state("sensor.battery", attribute="unit") @@ -64,7 +64,7 @@ def test_hainterface_get_state_cached(my_predbat=None): print(f"ERROR: Expected 'kWh', got '{result}'") failed += 1 else: - print("✓ Retrieved cached attribute") + print("PASS: Retrieved cached attribute") # Test with default for missing attribute result = ha_interface.get_state("sensor.battery", attribute="missing", default="default_value") @@ -72,7 +72,7 @@ def test_hainterface_get_state_cached(my_predbat=None): print(f"ERROR: Expected 'default_value', got '{result}'") failed += 1 else: - print("✓ Returned default for missing attribute") + print("PASS: Returned default for missing attribute") # Test raw mode result = ha_interface.get_state("sensor.battery", raw=True) @@ -80,7 +80,7 @@ def test_hainterface_get_state_cached(my_predbat=None): print("ERROR: raw=True should return full state dict") failed += 1 else: - print("✓ Returned raw state dict") + print("PASS: Returned raw state dict") return failed @@ -99,7 +99,7 @@ def test_hainterface_get_state_missing_entity(my_predbat=None): print(f"ERROR: Expected 'default_value', got '{result}'") failed += 1 else: - print("✓ Returned default for missing entity") + print("PASS: Returned default for missing entity") return failed @@ -119,7 +119,7 @@ def test_hainterface_get_state_case_insensitive(my_predbat=None): print("ERROR: Should be case-insensitive") failed += 1 else: - print("✓ Case-insensitive lookup works") + print("PASS: Case-insensitive lookup works") return failed @@ -141,7 +141,7 @@ def test_hainterface_get_state_db_mirror_tracking(my_predbat=None): print("ERROR: Entity should be added to db_mirror_list") failed += 1 else: - print("✓ Entity tracked in db_mirror_list") + print("PASS: Entity tracked in db_mirror_list") return failed @@ -168,7 +168,7 @@ def test_hainterface_update_state_item_basic(my_predbat=None): print("ERROR: State value incorrect") failed += 1 else: - print("✓ State cached correctly") + print("PASS: State cached correctly") return failed @@ -199,14 +199,14 @@ def test_hainterface_update_state_item_db_mirror(my_predbat=None): print("ERROR: DatabaseManager should be called") failed += 1 else: - print("✓ DatabaseManager set_state_db called") + print("PASS: DatabaseManager set_state_db called") # Verify state still cached if "sensor.battery" not in ha_interface.state_data: print("ERROR: State should also be cached") failed += 1 else: - print("✓ State cached alongside DB mirror") + print("PASS: State cached alongside DB mirror") return failed @@ -233,7 +233,7 @@ def test_hainterface_update_state_with_api(my_predbat=None): print("ERROR: API should be called") failed += 1 else: - print("✓ API called") + print("PASS: API called") # Verify state cached if "sensor.battery" not in ha_interface.state_data: @@ -243,7 +243,7 @@ def test_hainterface_update_state_with_api(my_predbat=None): print("ERROR: State value incorrect") failed += 1 else: - print("✓ State cached from API") + print("PASS: State cached from API") return failed @@ -271,7 +271,7 @@ def test_hainterface_update_state_db_primary(my_predbat=None): print("ERROR: DatabaseManager should be called") failed += 1 else: - print("✓ DatabaseManager queried") + print("PASS: DatabaseManager queried") # Verify state cached if "sensor.battery" not in ha_interface.state_data: @@ -281,7 +281,7 @@ def test_hainterface_update_state_db_primary(my_predbat=None): print("ERROR: State value incorrect") failed += 1 else: - print("✓ State cached from DB") + print("PASS: State cached from DB") return failed @@ -312,7 +312,7 @@ def test_hainterface_update_states_bulk(my_predbat=None): print("ERROR: API should be called") failed += 1 else: - print("✓ API called") + print("PASS: API called") # Verify both entities cached if len(ha_interface.state_data) != 2: @@ -322,7 +322,7 @@ def test_hainterface_update_states_bulk(my_predbat=None): print("ERROR: Missing entities in cache") failed += 1 else: - print("✓ All entities cached") + print("PASS: All entities cached") return failed @@ -353,14 +353,14 @@ def test_hainterface_update_states_db_primary(my_predbat=None): print("ERROR: DatabaseManager should be called") failed += 1 else: - print("✓ DatabaseManager queried") + print("PASS: DatabaseManager queried") # Verify entities cached if len(ha_interface.state_data) != 2: print(f"ERROR: Expected 2 entities, got {len(ha_interface.state_data)}") failed += 1 else: - print("✓ All entities cached from DB") + print("PASS: All entities cached from DB") return failed @@ -388,14 +388,14 @@ def test_hainterface_set_state_basic(my_predbat=None): print("ERROR: POST API should be called") failed += 1 else: - print("✓ POST API called") + print("PASS: POST API called") # Verify GET called (for update_state) if not mock_get.called: print("ERROR: GET API should be called for refresh") failed += 1 else: - print("✓ GET API called to refresh state") + print("PASS: GET API called to refresh state") return failed @@ -432,14 +432,14 @@ def test_hainterface_set_state_db_mirror(my_predbat=None): print("ERROR: Wrong parameters to set_state_db") failed += 1 else: - print("✓ DatabaseManager set_state_db called correctly") + print("PASS: DatabaseManager set_state_db called correctly") # Verify state cached if "sensor.battery" not in ha_interface.state_data: print("ERROR: State should be cached") failed += 1 else: - print("✓ State cached") + print("PASS: State cached") return failed @@ -468,14 +468,14 @@ def test_hainterface_set_state_db_primary(my_predbat=None): print("ERROR: API should not be called in DB primary mode") failed += 1 else: - print("✓ No API call in DB primary mode") + print("PASS: No API call in DB primary mode") # Verify DB called if len(mock_db.set_state_calls) == 0: print("ERROR: DatabaseManager should be called") failed += 1 else: - print("✓ DatabaseManager called") + print("PASS: DatabaseManager called") return failed @@ -499,7 +499,7 @@ def test_hainterface_db_mirror_list_tracking(my_predbat=None): print("ERROR: get_state should add to db_mirror_list") failed += 1 else: - print("✓ get_state adds to db_mirror_list") + print("PASS: get_state adds to db_mirror_list") # Test update_state adds to list ha_interface.db_mirror_list = {} @@ -509,7 +509,7 @@ def test_hainterface_db_mirror_list_tracking(my_predbat=None): print("ERROR: update_state should add to db_mirror_list") failed += 1 else: - print("✓ update_state adds to db_mirror_list") + print("PASS: update_state adds to db_mirror_list") # Test set_state adds to list ha_interface.db_mirror_list = {} @@ -523,7 +523,7 @@ def test_hainterface_db_mirror_list_tracking(my_predbat=None): print("ERROR: set_state should add to db_mirror_list") failed += 1 else: - print("✓ set_state adds to db_mirror_list") + print("PASS: set_state adds to db_mirror_list") return failed @@ -565,9 +565,9 @@ def run_hainterface_state_tests(my_predbat): print("\n" + "=" * 80) if failed == 0: - print("✅ All HAInterface state tests passed!") + print("PASS: All HAInterface state tests passed!") else: - print(f"❌ {failed} HAInterface state test(s) failed") + print(f"ERROR: {failed} HAInterface state test(s) failed") print("=" * 80 + "\n") return failed diff --git a/apps/predbat/tests/test_hainterface_websocket.py b/apps/predbat/tests/test_hainterface_websocket.py index eec916d27..0eb6bee52 100644 --- a/apps/predbat/tests/test_hainterface_websocket.py +++ b/apps/predbat/tests/test_hainterface_websocket.py @@ -84,7 +84,7 @@ async def mock_sleep(delay): print(f"ERROR: Expected access_token 'test_key'") failed += 1 else: - print("✓ Auth message sent correctly") + print("PASS: Auth message sent correctly") # Check subscriptions if len(mock_ws.send_json.call_args_list) < 3: @@ -99,14 +99,14 @@ async def mock_sleep(delay): print(f"ERROR: Expected state_changed event_type") failed += 1 else: - print("✓ State subscription sent correctly") + print("PASS: State subscription sent correctly") subscribe_service = mock_ws.send_json.call_args_list[2][0][0] if subscribe_service.get("event_type") != "call_service": print(f"ERROR: Expected call_service event_type") failed += 1 else: - print("✓ Service subscription sent correctly") + print("PASS: Service subscription sent correctly") # Note: api_started is set after subscriptions, but since we exit early with api_stop, # it may not be set in test environment. In real operation, socketLoop continues running. @@ -161,13 +161,13 @@ async def mock_sleep(delay): print("ERROR: Should log auth failure") failed += 1 else: - print("✓ Auth failure logged") + print("PASS: Auth failure logged") if ha_interface.websocket_active: print("ERROR: websocket_active should be False after auth failure") failed += 1 else: - print("✓ websocket_active set to False") + print("PASS: websocket_active set to False") return failed @@ -239,7 +239,7 @@ async def mock_sleep(delay): print(f"ERROR: Expected state '200', got '{state.get('state')}'") failed += 1 else: - print("✓ State updated correctly") + print("PASS: State updated correctly") if not mock_base.trigger_watch_list_calls: print("ERROR: trigger_watch_list should be called") @@ -253,7 +253,7 @@ async def mock_sleep(delay): print(f"ERROR: Expected new_state '200'") failed += 1 else: - print("✓ trigger_watch_list called correctly") + print("PASS: trigger_watch_list called correctly") return failed @@ -329,7 +329,7 @@ async def mock_sleep(delay): print(f"ERROR: Expected service 'turn_on'") failed += 1 else: - print("✓ trigger_callback called correctly") + print("PASS: trigger_callback called correctly") return failed @@ -385,7 +385,7 @@ async def mock_sleep(delay): print("ERROR: Should log result failure") failed += 1 else: - print("✓ Result failure logged") + print("PASS: Result failure logged") return failed @@ -438,7 +438,7 @@ async def mock_sleep(delay): print("ERROR: Should log reconnect attempt") failed += 1 else: - print("✓ Reconnect attempt logged") + print("PASS: Reconnect attempt logged") return failed @@ -493,14 +493,14 @@ async def mock_sleep(delay): print("ERROR: Should log error messages") failed += 1 else: - print("✓ Error logging present") + print("PASS: Error logging present") # fatal_error_occurred is called when error_count reaches 10 if not mock_base.fatal_error_occurred_called: print("WARN: fatal_error_occurred not called (may exit before check)") # Don't fail on this - timing dependent else: - print("✓ fatal_error_occurred called") + print("PASS: fatal_error_occurred called") return failed @@ -547,21 +547,21 @@ async def mock_sleep(delay): print(f"Logs: {[log for log in mock_base.log_messages if 'fail' in log.lower()]}") failed += 1 else: - print("✓ 'failed 10 times' logged") + print("PASS: 'failed 10 times' logged") # Check that fatal_error_occurred was called if not mock_base.fatal_error_occurred_called: print("ERROR: fatal_error_occurred should be called") failed += 1 else: - print("✓ fatal_error_occurred called") + print("PASS: fatal_error_occurred called") # Verify it made around 10 connection attempts if connection_attempts[0] < 9: print(f"ERROR: Expected ~10 connection attempts, got {connection_attempts[0]}") failed += 1 else: - print(f"✓ Made {connection_attempts[0]} connection attempts") + print(f"PASS: Made {connection_attempts[0]} connection attempts") return failed @@ -614,7 +614,7 @@ async def mock_sleep(delay): print("ERROR: Should log exception") failed += 1 else: - print("✓ Exception logged") + print("PASS: Exception logged") return failed @@ -651,7 +651,7 @@ async def mock_sleep(delay): print("ERROR: Should log startup exception") failed += 1 else: - print("✓ Startup exception logged") + print("PASS: Startup exception logged") return failed @@ -700,7 +700,7 @@ async def mock_sleep(delay): print("ERROR: update_pending should be True after connection") failed += 1 else: - print("✓ update_pending set to True") + print("PASS: update_pending set to True") return failed @@ -767,28 +767,28 @@ async def mock_sleep(delay): print("ERROR: threading.Event for queued command was not set after connection drop") failed += 1 else: - print("✓ threading.Event set when connection dropped with command in queue") + print("PASS: threading.Event set when connection dropped with command in queue") # The error must be 'connection_lost' if pre_queued_result.get("error") != "connection_lost": print(f"ERROR: Expected error='connection_lost', got {pre_queued_result.get('error')!r}") failed += 1 else: - print("✓ result_holder error set to 'connection_lost'") + print("PASS: result_holder error set to 'connection_lost'") # success must be False if pre_queued_result.get("success") is not False: print(f"ERROR: Expected success=False, got {pre_queued_result.get('success')!r}") failed += 1 else: - print("✓ result_holder success set to False") + print("PASS: result_holder success set to False") # The queue must be empty after cleanup if ha_interface.ws_command_queue: print("ERROR: ws_command_queue should be empty after connection drop cleanup") failed += 1 else: - print("✓ ws_command_queue cleared after connection drop") + print("PASS: ws_command_queue cleared after connection drop") return failed @@ -848,7 +848,7 @@ async def mock_sleep(delay): print("ERROR: service_registered event should be fired") failed += 1 else: - print("✓ service_registered event fired") + print("PASS: service_registered event fired") return failed @@ -876,9 +876,9 @@ def run_hainterface_websocket_tests(my_predbat): print("\n" + "=" * 80) if failed == 0: - print("✅ All HAInterface websocket tests passed!") + print("PASS: All HAInterface websocket tests passed!") else: - print(f"❌ {failed} HAInterface websocket test(s) failed") + print(f"ERROR: {failed} HAInterface websocket test(s) failed") print("=" * 80) return failed diff --git a/apps/predbat/tests/test_integer_config.py b/apps/predbat/tests/test_integer_config.py index 893ea5bee..d145f5fe2 100644 --- a/apps/predbat/tests/test_integer_config.py +++ b/apps/predbat/tests/test_integer_config.py @@ -46,7 +46,7 @@ def test_integer_config_entities(my_predbat): assert isinstance(ha_value, int), f"Value 2.0 with step=1 should convert to int, got {type(ha_value)}" assert ha_value == 2, f"Value should be 2, got {ha_value}" - print(f"✓ Float 2.0 with step=1 converts to integer 2") + print(f"PASS: Float 2.0 with step=1 converts to integer 2") # Test 2: String integer like "3" should convert to int ha_value = "3" @@ -57,7 +57,7 @@ def test_integer_config_entities(my_predbat): assert isinstance(ha_value, int), f"Value '3' with step=1 should convert to int, got {type(ha_value)}" assert ha_value == 3, f"Value should be 3, got {ha_value}" - print(f"✓ String '3' with step=1 converts to integer 3") + print(f"PASS: String '3' with step=1 converts to integer 3") # Test 3: String float like "4.0" should convert to int for integer step ha_value = "4.0" @@ -68,7 +68,7 @@ def test_integer_config_entities(my_predbat): assert isinstance(ha_value, int), f"Value '4.0' with step=1 should convert to int, got {type(ha_value)}" assert ha_value == 4, f"Value should be 4, got {ha_value}" - print(f"✓ String '4.0' with step=1 converts to integer 4") + print(f"PASS: String '4.0' with step=1 converts to integer 4") # Test 4: Float with decimal part should stay as float even for integer step ha_value = 4.5 @@ -79,7 +79,7 @@ def test_integer_config_entities(my_predbat): assert isinstance(ha_value, float), f"Value 4.5 should remain float, got {type(ha_value)}" assert ha_value == 4.5, f"Value should be 4.5, got {ha_value}" - print(f"✓ Float 4.5 stays as float 4.5 (has fractional part)") + print(f"PASS: Float 4.5 stays as float 4.5 (has fractional part)") # Test 5: Test with decimal step - should always stay as float # Find an entity with decimal step @@ -101,7 +101,7 @@ def test_integer_config_entities(my_predbat): # With decimal step, conversion shouldn't happen assert isinstance(ha_value, float), f"Value 5.0 with step={decimal_step} should stay float, got {type(ha_value)}" - print(f"✓ Float 5.0 with step={decimal_step} stays as float") + print(f"PASS: Float 5.0 with step={decimal_step} stays as float") else: print("! No decimal step entity found to test") @@ -124,9 +124,9 @@ def test_integer_config_entities(my_predbat): assert isinstance(ha_value, int), f"set_reserve_min 27.0 should convert to int, got {type(ha_value)}" assert ha_value == 27, f"Value should be 27, got {ha_value}" - print(f"✓ set_reserve_min: Float 27.0 with step=1 converts to integer 27") + print(f"PASS: set_reserve_min: Float 27.0 with step=1 converts to integer 27") - print("✓ Test passed: Integer conversion logic works correctly") + print("PASS: Test passed: Integer conversion logic works correctly") return False @@ -149,7 +149,7 @@ def test_expose_config_preserves_integer(my_predbat): # State should be integer 5, not float 5.0 assert isinstance(state_value, int), f"Exposed state should be int, got {type(state_value)}: {state_value}" assert state_value == 5, f"Exposed state should be 5, got {state_value}" - print(f"✓ expose_config correctly writes integer 5") + print(f"PASS: expose_config correctly writes integer 5") # Test with set_reserve_min my_predbat.expose_config("set_reserve_min", 30, force_ha=True) @@ -162,7 +162,7 @@ def test_expose_config_preserves_integer(my_predbat): assert isinstance(state_value, int), f"Exposed set_reserve_min should be int, got {type(state_value)}: {state_value}" assert state_value == 30, f"Exposed set_reserve_min should be 30, got {state_value}" - print(f"✓ expose_config correctly writes integer 30 for set_reserve_min") + print(f"PASS: expose_config correctly writes integer 30 for set_reserve_min") # Test with a float value to ensure floats still work # Find an entity with decimal step @@ -186,7 +186,7 @@ def test_expose_config_preserves_integer(my_predbat): assert isinstance(state_value, float), f"Exposed {decimal_name} should be float, got {type(state_value)}: {state_value}" assert state_value == 12.75, f"Exposed {decimal_name} should be 12.75, got {state_value}" - print(f"✓ expose_config correctly writes float 12.75 for {decimal_name}") + print(f"PASS: expose_config correctly writes float 12.75 for {decimal_name}") else: print("! No decimal step entity found to test") @@ -194,7 +194,7 @@ def test_expose_config_preserves_integer(my_predbat): for item in my_predbat.CONFIG_ITEMS: my_predbat.expose_config(item.get("name"), item.get("default"), force_ha=True) - print("✓ Test passed: expose_config preserves type correctly") + print("PASS: Test passed: expose_config preserves type correctly") return False diff --git a/apps/predbat/tests/test_kraken.py b/apps/predbat/tests/test_kraken.py index d710ec919..cd1dd9d1e 100644 --- a/apps/predbat/tests/test_kraken.py +++ b/apps/predbat/tests/test_kraken.py @@ -394,8 +394,8 @@ def test_run_refetches_rates_when_stale(): api = make_kraken_api() api.current_tariff = {"tariff_code": "E-1R-VAR-01", "product_code": "VAR-01"} - api.tariff_fetched_at = datetime.now() # fresh tariff → no re-discovery - api.rates_fetched_at = datetime.now() - timedelta(minutes=KRAKEN_RATES_REFRESH_MINUTES + 5) # stale → re-fetch + api.tariff_fetched_at = datetime.now() # fresh tariff -> no re-discovery + api.rates_fetched_at = datetime.now() - timedelta(minutes=KRAKEN_RATES_REFRESH_MINUTES + 5) # stale -> re-fetch api.async_find_tariffs = AsyncMock(return_value=None) api.async_fetch_rates = AsyncMock(return_value=[{"value_inc_vat": 24.5}]) api.async_fetch_standing_charges = AsyncMock(return_value=53.0) @@ -546,7 +546,7 @@ def test_standing_charge_converts_pence_to_pounds(): with patch("aiohttp.ClientSession", return_value=mock_session): result = asyncio.run(api.async_fetch_standing_charges()) - # 53.0 pence → 0.53 pounds + # 53.0 pence -> 0.53 pounds assert result == 0.53 @@ -644,7 +644,7 @@ def test_fetch_standing_charges_graphql_returns_value(): result = asyncio.run(api.async_fetch_standing_charges_graphql("1900000000456")) - # 61.95 pence/day → 0.6195 pounds/day + # 61.95 pence/day -> 0.6195 pounds/day assert result is not None assert abs(result - 0.6195) < 1e-6 @@ -999,7 +999,7 @@ async def capture_query(query, context): expected_start = midnight_utc - timedelta(days=1) assert abs((start_dt - expected_start).total_seconds()) < 60, f"start_at {start_dt} not close to expected {expected_start}" - # forecast_hours=72 → forecast_days=3 → end_at must be midnight + 4 days + # forecast_hours=72 -> forecast_days=3 -> end_at must be midnight + 4 days expected_end = midnight_utc + timedelta(days=4) assert abs((end_dt - expected_end).total_seconds()) < 60, f"end_at {end_dt} not close to expected {expected_end}" @@ -1007,7 +1007,7 @@ async def capture_query(query, context): def test_fetch_rates_graphql_window_default_forecast_hours(): """async_fetch_rates_graphql() uses forecast_hours default of 48 when not configured. - forecast_hours=48 → forecast_days=2 → end_at = midnight + 3 days. + forecast_hours=48 -> forecast_days=2 -> end_at = midnight + 3 days. """ from datetime import datetime, timedelta, timezone import re @@ -1035,7 +1035,7 @@ async def capture_query(query, context): now = datetime.now(timezone.utc) midnight_utc = now.replace(hour=0, minute=0, second=0, microsecond=0) - # default forecast_hours=48 → forecast_days=2 → end_at must be midnight + 3 days + # default forecast_hours=48 -> forecast_days=2 -> end_at must be midnight + 3 days expected_end = midnight_utc + timedelta(days=3) assert abs((end_dt - expected_end).total_seconds()) < 60, f"end_at {end_dt} not close to expected {expected_end}" @@ -1296,7 +1296,7 @@ def test_build_rest_auth_uses_basic_auth_for_api_key(): """_build_rest_auth returns HTTP Basic auth (API key as username) in api_key mode.""" import aiohttp - api = make_kraken_api() # api_key mode → _api_key = "test-key" + api = make_kraken_api() # api_key mode -> _api_key = "test-key" auth, headers = asyncio.run(api._build_rest_auth()) assert isinstance(auth, aiohttp.BasicAuth) assert auth.login == "test-key" @@ -1322,7 +1322,7 @@ def test_fetch_rates_404_retries_authenticated_and_succeeds(): Reproduces the live EDF case: E-1R-EDF_EXPORT_SEG_12M_HH-B (a private product) 404s unauthenticated, so the authenticated retry is what actually recovers the export rates. """ - api = make_kraken_api() # api_key mode → authenticated retry uses HTTP Basic auth + api = make_kraken_api() # api_key mode -> authenticated retry uses HTTP Basic auth export_tariff = {"tariff_code": "E-1R-EDF_EXPORT_SEG_12M_HH-B", "product_code": "EDF_EXPORT_SEG_12M"} api.export_tariff = export_tariff api.export_mpan = "1170001829927" @@ -1404,7 +1404,7 @@ def test_connection_nodes_extracts_edges(): assert KrakenAPI._connection_nodes(conn) == [{"value": 1}, {"value": 2}] # Backward-compat: a plain list is returned unchanged. assert KrakenAPI._connection_nodes([{"value": 3}]) == [{"value": 3}] - # Empty / missing shapes → []. + # Empty / missing shapes -> []. assert KrakenAPI._connection_nodes(None) == [] assert KrakenAPI._connection_nodes({}) == [] assert KrakenAPI._connection_nodes({"edges": []}) == [] @@ -1551,7 +1551,7 @@ def test_run_first_restores_cache_and_skips_fetch(): result = asyncio.run(api.run(0, True)) assert result is True - # Fresh cache → no API calls at all. + # Fresh cache -> no API calls at all. api.async_find_tariffs.assert_not_called() api.async_fetch_rates.assert_not_called() api.async_fetch_standing_charges.assert_not_called() @@ -1603,7 +1603,7 @@ def test_normalize_dispatches_field_mapping(): planned = api._normalize_dispatches( [ {"start": "2026-07-08T00:00:00Z", "end": "2026-07-08T00:30:00Z", "type": "SMART", "energyAddedKwh": "2.5"}, - {"start": None, "end": "x"}, # missing start → dropped + {"start": None, "end": "x"}, # missing start -> dropped ], completed=False, ) @@ -1627,7 +1627,7 @@ def test_normalize_dispatches_trims_in_progress_planned(): planned = api._normalize_dispatches([{"start": start, "end": end, "type": "SMART", "energyAddedKwh": 10.0}], completed=False) assert len(planned) == 1 - # ~half the window remains → ~half the energy, and start advanced to ~now. + # ~half the window remains -> ~half the energy, and start advanced to ~now. assert 4.5 < planned[0]["charge_in_kwh"] < 5.5 trimmed_start = datetime.fromisoformat(planned[0]["start"]) assert abs((trimmed_start - now).total_seconds()) < 60 @@ -1722,8 +1722,8 @@ def test_run_fetches_and_wires_dispatches(): api = make_kraken_api() api.current_tariff = {"tariff_code": "E-1R-VAR-01", "product_code": "VAR-01"} api.import_rates = [{"value_inc_vat": 24.5}] - api.tariff_fetched_at = datetime.now() # fresh → no tariff work / device re-discovery - api.rates_fetched_at = datetime.now() # fresh → no rate work + api.tariff_fetched_at = datetime.now() # fresh -> no tariff work / device re-discovery + api.rates_fetched_at = datetime.now() # fresh -> no rate work api.dispatch_fetched_at = None # dispatch due api.intelligent_devices = {"dev-1": {"device_id": "dev-1", "planned_dispatches": [], "completed_dispatches": []}} api.async_find_tariffs = AsyncMock(return_value=None) @@ -1760,7 +1760,7 @@ def test_run_no_devices_does_not_save_cache_every_cycle(): api.import_rates = [{"value_inc_vat": 24.5}] api.tariff_fetched_at = datetime.now() # fresh api.rates_fetched_at = datetime.now() # fresh - api.dispatch_fetched_at = None # no devices → never set → dispatch_due would be True + api.dispatch_fetched_at = None # no devices -> never set -> dispatch_due would be True api.intelligent_devices = {} # common no-EV account api.async_find_tariffs = AsyncMock(return_value=None) api.save_kraken_cache = AsyncMock() diff --git a/apps/predbat/tests/test_load_ml.py b/apps/predbat/tests/test_load_ml.py index c8eea6dd7..e99165092 100644 --- a/apps/predbat/tests/test_load_ml.py +++ b/apps/predbat/tests/test_load_ml.py @@ -817,7 +817,7 @@ def _test_curriculum_training(): now_utc = datetime.now(timezone.utc) - # ── Happy-path: 28 days of data → 3 intermediate passes + 1 final ──────── + # ── Happy-path: 28 days of data -> 3 intermediate passes + 1 final ──────── np.random.seed(42) load_data_28 = _create_synthetic_load_data(n_days=28, now_utc=now_utc) @@ -881,7 +881,7 @@ def _test_curriculum_training(): now_utc, epochs=3, patience=3, - curriculum_window_days=14, # Requires 14 days; only 5 days available → fallback + curriculum_window_days=14, # Requires 14 days; only 5 days available -> fallback curriculum_step_days=7, ) # Fallback should complete without crashing and return a MAE or None (if still @@ -1078,7 +1078,7 @@ async def mock_save_database_history(): component.save_database_history = mock_save_database_history # ── Run 1 (first=True, seconds=0) ─────────────────────────────────────── - # Expect: data fetched and stored, but training deferred → no save, no training. + # Expect: data fetched and stored, but training deferred -> no save, no training. component._fetch_load_data = AsyncMock(return_value=(fetch_data_1, 28, 5.0, None, None, None, None)) result = await component.run(seconds=0, first=True) @@ -1093,13 +1093,13 @@ async def mock_save_database_history(): assert save_call_count[0] == 0, f"save_database_history should NOT be called on first run (deferred), called {save_call_count[0]} times" assert training_call_count[0] == 0, f"Training should NOT run on first run, ran {training_call_count[0]} times" - print(" \u2713 Run 1: data populated, training deferred, no save") + print(" PASS: Run 1: data populated, training deferred, no save") # ── Advance time by ELAPSED_MINUTES ───────────────────────────────────── mock_base.now_utc = mock_base.now_utc + timedelta(minutes=ELAPSED_MINUTES) # ── Run 2 (first=False, seconds=30) ───────────────────────────────────── - # last_train_time is None → retrain_age_seconds = RETRAIN_INTERVAL_SECONDS → should_train=True + # last_train_time is None -> retrain_age_seconds = RETRAIN_INTERVAL_SECONDS -> should_train=True # Expect: shift old keys, merge fresh data, run initial training, save once. component._fetch_load_data = AsyncMock(return_value=(fetch_data_2, 7, 3.0, None, None, None, None)) @@ -1111,9 +1111,9 @@ async def mock_save_database_history(): assert save_call_count[0] == 1, f"save_database_history should be called once after second run, called {save_call_count[0]} times" # ── Verify time-shift: old keys shifted forward by ELAPSED_MINUTES ── - assert component.load_data.get(SHIFT) == 0.1, f"Old key 0 → key {SHIFT} after shift, got {component.load_data.get(SHIFT)}" - assert component.load_data.get(500 + SHIFT) == 0.1, f"Old key 500 → key {500 + SHIFT}, got {component.load_data.get(500 + SHIFT)}" - assert component.load_data.get(2000 + SHIFT) == 0.05, f"Old key 2000 → key {2000 + SHIFT}, got {component.load_data.get(2000 + SHIFT)}" + assert component.load_data.get(SHIFT) == 0.1, f"Old key 0 -> key {SHIFT} after shift, got {component.load_data.get(SHIFT)}" + assert component.load_data.get(500 + SHIFT) == 0.1, f"Old key 500 -> key {500 + SHIFT}, got {component.load_data.get(500 + SHIFT)}" + assert component.load_data.get(2000 + SHIFT) == 0.05, f"Old key 2000 -> key {2000 + SHIFT}, got {component.load_data.get(2000 + SHIFT)}" assert component.load_data.get(2000) is None, f"Key 2000 should be gone after shift (moved to {2000 + SHIFT})" # ── Verify fresh data from fetch_data_2 is at the expected keys ── @@ -1121,15 +1121,15 @@ async def mock_save_database_history(): actual = component.load_data.get(minute) assert actual == expected_value, f"At minute {minute}: expected {expected_value} (fetch_data_2) but got {actual}" - print(" \u2713 Run 2: initial training fired, keys shifted, fresh data merged, save called") + print(" PASS: Run 2: initial training fired, keys shifted, fresh data merged, save called") # ── Advance time by another ELAPSED_MINUTES ────────────────────────────── mock_base.now_utc = mock_base.now_utc + timedelta(minutes=ELAPSED_MINUTES) # ── Run 3 (first=False, seconds=PREDICTION_INTERVAL_SECONDS) ───────────── # last_train_time = 30 min ago (set in mock_do_training to component.now_utc of Run 2). - # retrain_age_seconds = 30*60 = 1800 < RETRAIN_INTERVAL_SECONDS (7200) → should_train=False. - # seconds % PREDICTION_INTERVAL_SECONDS == 0 → should_fetch=True. + # retrain_age_seconds = 30*60 = 1800 < RETRAIN_INTERVAL_SECONDS (7200) -> should_train=False. + # seconds % PREDICTION_INTERVAL_SECONDS == 0 -> should_fetch=True. # Expect: fetch+predict+save only, no training. component._fetch_load_data = AsyncMock(return_value=(fetch_data_3, 7, 3.0, None, None, None, None)) @@ -1139,7 +1139,7 @@ async def mock_save_database_history(): assert training_call_count[0] == 1, f"Training should NOT run again on third run (model too fresh), ran {training_call_count[0]} times total" assert save_call_count[0] == 2, f"save_database_history should be called again on third run, called {save_call_count[0]} times" - print(" \u2713 Run 3: fetch-only cycle (model fresh), save called without retraining") + print(" PASS: Run 3: fetch-only cycle (model fresh), save called without retraining") run_async(run_test()) @@ -1913,7 +1913,7 @@ async def test_basic_fetch(): assert result_age == 28, f"Expected 28 days, got {result_age}" assert len(result_data) > 0, "Load data should not be empty" assert result_now >= 0, f"Current load should be non-negative, got {result_now}" - print(" ✓ Basic fetch successful") + print(" PASS: Basic fetch successful") # Test 2: Missing sensor (should return None) async def test_missing_sensor(): @@ -1949,7 +1949,7 @@ def get_arg(self, key, default=None, indirect=True, combine=False, attribute=Non assert result_data is None, "Should return None when sensor missing" assert result_age == 0, "Age should be 0 when sensor missing" assert result_now == 0, "Current load should be 0 when sensor missing" - print(" ✓ Missing sensor handled correctly") + print(" PASS: Missing sensor handled correctly") # Test 3: Car charging subtraction async def test_car_charging_subtraction(): @@ -2031,7 +2031,7 @@ def mock_get_arg_with_car(key, default=None, indirect=True, combine=False, attri expected = 0.7 # One step of (1.0 - 0.3) in per-step format assert abs(value_1435 - expected) < 0.01, f"At minute 1435, expected ~{expected} kWh, got {value_1435:.4f}" - print(" ✓ Car charging subtraction works") + print(" PASS: Car charging subtraction works") # Test 3b: Car charging threshold-based detection (without sensor) async def test_car_charging_threshold_detection(): @@ -2130,7 +2130,7 @@ def mock_get_arg_threshold(key, default=None, indirect=True, combine=False, attr expected = 0.375 # Car detected, subtract estimate assert abs(value_1420 - expected) < 0.01, f"At minute 1420 (high load again), expected ~{expected} kWh, got {value_1420:.4f}" - print(" ✓ Car charging threshold detection works") + print(" PASS: Car charging threshold detection works") # Test 4: Load power fill async def test_load_power_fill(): @@ -2171,7 +2171,7 @@ def mock_get_arg_with_power(key, default=None, indirect=True, combine=False, att assert result_data is not None, "Should return load data" assert mock_base_with_power.fill_load_from_power.called, "fill_load_from_power should be called" assert result_now >= 0, f"Current load should be non-negative, got {result_now}" - print(" ✓ Load power fill invoked") + print(" PASS: Load power fill invoked") # Test 5: Exception handling async def test_exception_handling(): @@ -2194,7 +2194,7 @@ async def test_exception_handling(): assert result_data is None, "Should return None on exception" assert result_age == 0, "Age should be 0 on exception" assert result_now == 0, "Current load should be 0 on exception" - print(" ✓ Exception handling works") + print(" PASS: Exception handling works") # Test 6: Empty load data async def test_empty_load_data(): @@ -2218,7 +2218,7 @@ async def test_empty_load_data(): assert result_data is None, "Should return None when load data is empty" assert result_age == 0, "Age should be 0 when load data is empty" assert result_now == 0, "Current load should be 0 when load data is empty" - print(" ✓ Empty load data handled correctly") + print(" PASS: Empty load data handled correctly") # Test 7: Temperature data fetch with future predictions only async def test_temperature_data_fetch(): @@ -2275,7 +2275,7 @@ def mock_get_state_wrapper_side_effect(entity_id, default=None, attribute=None, # Verify get_state_wrapper was called correctly assert mock_base_with_temp.get_state_wrapper.called, "get_state_wrapper should be called" - print(" ✓ Temperature data fetch (future predictions) works") + print(" PASS: Temperature data fetch (future predictions) works") # Test 8: Temperature data with no predictions (None return) async def test_temperature_no_data(): @@ -2305,7 +2305,7 @@ async def test_temperature_no_data(): assert isinstance(result_temp, dict), "Temperature data should be a dict" assert len(result_temp) == 0, "Temperature data should be empty when no predictions available" - print(" ✓ Temperature data with no predictions handled correctly") + print(" PASS: Temperature data with no predictions handled correctly") # Test 9: Step-size calculation correctness (bug #3384 regression test) async def test_step_size_calculation(): @@ -2400,7 +2400,7 @@ def mock_get_arg_no_car_hold(key, default=None, indirect=True, combine=False, at # Verify current load is reasonable (not near-zero) assert result_now > 0.05, f"Current load should be > 0.05 kWh (got {result_now:.4f}). Bug #3384 would cause near-zero." - print(" ✓ Step-size calculation correct (bug #3384 regression test passed)") + print(" PASS: Step-size calculation correct (bug #3384 regression test passed)") # Run all sub-tests print(" Running LoadMLComponent._fetch_load_data tests:") @@ -2589,7 +2589,7 @@ def mock_dashboard_item(entity_id, state, attributes, app): assert attrs["icon"] == "mdi:chart-line", "icon should be 'mdi:chart-line'" assert attrs2["icon"] == "mdi:chart-line", "icon should be 'mdi:chart-line'" - print(" ✓ Entity published with correct attributes") + print(" PASS: Entity published with correct attributes") # Test 2: Empty predictions mock_base.dashboard_calls = [] @@ -2602,7 +2602,7 @@ def mock_dashboard_item(entity_id, state, attributes, app): assert call2["state"] == 0, "State should be 0 with empty predictions" assert call["attributes"]["results"] == {}, "results should be empty dict" - print(" ✓ Empty predictions handled correctly") + print(" PASS: Empty predictions handled correctly") print(" All _publish_entity tests passed!") @@ -2920,7 +2920,7 @@ def get_arg(self, key, default=None, indirect=True, combine=False, attribute=Non now_utc = datetime.now(timezone.utc) load_data = _create_synthetic_load_data(n_days=7, now_utc=now_utc) - # --- Part 1: model with a known timestamp → last_train_time set to that timestamp --- + # --- Part 1: model with a known timestamp -> last_train_time set to that timestamp --- predictor = LoadPredictor(learning_rate=0.01) predictor.train(load_data, now_utc, is_initial=True, epochs=2, time_decay_days=7) known_timestamp = predictor.training_timestamp @@ -2935,7 +2935,7 @@ def get_arg(self, key, default=None, indirect=True, combine=False, attribute=Non assert component.model_valid is True, "Model should be marked valid" assert component.initial_training_done is True, "initial_training_done should be True" - # --- Part 2: model with no timestamp → last_train_time stays None (triggers retrain) --- + # --- Part 2: model with no timestamp -> last_train_time stays None (triggers retrain) --- predictor2 = LoadPredictor(learning_rate=0.01) predictor2.train(load_data, now_utc, is_initial=True, epochs=2, time_decay_days=7) predictor2.training_timestamp = None # Simulate a pre-timestamp model diff --git a/apps/predbat/tests/test_minute_array.py b/apps/predbat/tests/test_minute_array.py index 6ee51814b..0b1f7e7ec 100644 --- a/apps/predbat/tests/test_minute_array.py +++ b/apps/predbat/tests/test_minute_array.py @@ -128,7 +128,7 @@ def test_minute_array(my_predbat): # Test 11: pad=False sizing — max(dict.keys()) + 2 captures all accumulated keys print("Test 11: pad=False size derived from max(dict.keys()) + 2") # Simulate two-entity accumulation: entity A adds keys 0..4, entity B adds keys 0..2 - # Final dict has keys 0..4 but age_days would reflect entity B (2 days → 2*1440 keys) + # Final dict has keys 0..4 but age_days would reflect entity B (2 days -> 2*1440 keys) big_dict = {i: float(i) for i in range(5)} size = max(big_dict.keys()) + 2 # 4 + 2 = 6 ma11 = MinuteArray(big_dict, size) diff --git a/apps/predbat/tests/test_minute_data.py b/apps/predbat/tests/test_minute_data.py index 9272b9c51..28a4a8301 100644 --- a/apps/predbat/tests/test_minute_data.py +++ b/apps/predbat/tests/test_minute_data.py @@ -42,10 +42,10 @@ def test_minute_data(my_predbat): # older period (transition+1..minutes_to) is filled with last_state. # Gap-fill propagates newest state (30.0) back to minute 0. # minute 0: gap-filled with newest=30.0 - # minute 5: transition minute for item@12:00 → state=30.0 - # minute 34: fill from item@12:00 → last_state=20.0 - # minute 35: transition minute for item@11:30 → state=20.0 - # minute 65: transition minute for item@11:00 → state=10.0 + # minute 5: transition minute for item@12:00 -> state=30.0 + # minute 34: fill from item@12:00 -> last_state=20.0 + # minute 35: transition minute for item@11:30 -> state=20.0 + # minute 65: transition minute for item@11:00 -> state=10.0 expected_points = [30.0, 30.0, 20.0, 20.0, 10.0] # Check that we have data and it's reasonable @@ -428,7 +428,7 @@ def test_minute_data(my_predbat): # Item@11:00 (2.5kW=2500W): first item, transition at minute 60 only (minutes==minutes_to) # Gap-fill propagates newest (3500W) back to minute 0. - # Minute 15: transition minute for item@11:45 → state=3500W + # Minute 15: transition minute for item@11:45 -> state=3500W if 15 not in result_data: print("ERROR: kW to W conversion test failed - no data at minute 15") failed = True @@ -436,7 +436,7 @@ def test_minute_data(my_predbat): print("ERROR: kW to W conversion test failed - expected 3500 W at minute 15, got {}".format(result_data[15])) failed = True - # Minute 30: transition minute for item@11:30 → state=3000W + # Minute 30: transition minute for item@11:30 -> state=3000W if 30 not in result_data: print("ERROR: kW to W conversion test failed - no data at minute 30") failed = True @@ -444,7 +444,7 @@ def test_minute_data(my_predbat): print("ERROR: kW to W conversion test failed - expected 3000 W at minute 30, got {}".format(result_data[30])) failed = True - # Minute 45: fill from item@11:30 → last_state=2500W + # Minute 45: fill from item@11:30 -> last_state=2500W if 45 not in result_data: print("ERROR: kW to W conversion test failed - no data at minute 45") failed = True @@ -865,9 +865,9 @@ def test_minute_data_no_smoothing_backwards(my_predbat): # ------------------------------------------------------------------ # now = 12:00, item-1 at 11:30 (30 min ago), item-2 at 11:50 (10 min ago). # Expected layout (minutes ago from now): - # 0-9 → 20.0 (newest state, gap-filled by the post-process logic) - # 10 → 20.0 (transition minute for item-2, state written explicitly) - # 11-30 → 10.0 (older-period fill with last_state from item-1) + # 0-9 -> 20.0 (newest state, gap-filled by the post-process logic) + # 10 -> 20.0 (transition minute for item-2, state written explicitly) + # 11-30 -> 10.0 (older-period fill with last_state from item-1) print("Test 1: two entries – transition minute and older-period fill") now = datetime(2024, 10, 4, 12, 0, 0, tzinfo=utc) history = [ @@ -902,9 +902,9 @@ def test_minute_data_no_smoothing_backwards(my_predbat): # ------------------------------------------------------------------ # now = 12:00, items at 11:00 (60 min), 11:20 (40 min), 11:40 (20 min) # Expected: - # 0-20 → 20.0 (newest; transition at 20 written explicitly then gap-filled forward) - # 21-40 → 10.0 (fill for item-3's older period) - # 41-60 → 5.0 (fill for item-2's older period) + # 0-20 -> 20.0 (newest; transition at 20 written explicitly then gap-filled forward) + # 21-40 -> 10.0 (fill for item-3's older period) + # 41-60 -> 5.0 (fill for item-2's older period) print("Test 2: three entries – chained transitions") history3 = [ {"state": "5.0", "last_updated": "2024-10-04T11:00:00+00:00"}, # 60 min ago @@ -916,7 +916,7 @@ def test_minute_data_no_smoothing_backwards(my_predbat): checks3 = { 0: 20.0, # gap-filled with newest 20: 20.0, # transition minute for item-3 - 21: 10.0, # start of older-period fill (item-3 → item-2's state) + 21: 10.0, # start of older-period fill (item-3 -> item-2's state) 40: 10.0, # last minute of that fill (item-2 transition is overwritten by fill from item-3) 41: 5.0, # start of item-2's older-period fill 60: 5.0, # last minute (item-1 set minutes==minutes_to, then overwritten to 5.0 by item-2's fill) @@ -1009,8 +1009,8 @@ def test_minute_data_no_smoothing_forward(my_predbat): # ------------------------------------------------------------------ # With to_key set, the gap-fill logic does NOT run (guarded by `if not to_key:`). # Only the explicitly-defined half-open windows are populated. - # Window 1: last_updated=12:10, last_changed=12:20 → mdata[10..19] = 10.0 - # Window 2: last_updated=12:20, last_changed=12:40 → mdata[20..39] = 20.0 + # Window 1: last_updated=12:10, last_changed=12:20 -> mdata[10..19] = 10.0 + # Window 2: last_updated=12:20, last_changed=12:40 -> mdata[20..39] = 20.0 # Anything outside those ranges is absent from the result dict. print("Test 1: forward with to_key – state fills half-open window [start, end)") history_fwd = [ @@ -1129,19 +1129,19 @@ def test_minute_data_no_smoothing_forward(my_predbat): smoothing=False, ) - # Window 1 → 5..9 = 3.0 + # Window 1 -> 5..9 = 3.0 for m in range(5, 10): if result_gap.get(m) != 3.0: print(f"ERROR: Test 4 failed – minute {m} (window 1) expected 3.0, got {result_gap.get(m)}") failed = True break - # Gap → 10..19 is absent because to_key prevents gap-fill + # Gap -> 10..19 is absent because to_key prevents gap-fill for m in range(10, 20): if result_gap.get(m) is not None: print(f"ERROR: Test 4 failed – minute {m} (gap) expected None (no fill with to_key), got {result_gap.get(m)}") failed = True break - # Window 2 → 20..29 = 9.0 + # Window 2 -> 20..29 = 9.0 for m in range(20, 30): if result_gap.get(m) != 9.0: print(f"ERROR: Test 4 failed – minute {m} (window 2) expected 9.0, got {result_gap.get(m)}") diff --git a/apps/predbat/tests/test_ml_load_fallback.py b/apps/predbat/tests/test_ml_load_fallback.py new file mode 100644 index 000000000..df7500cba --- /dev/null +++ b/apps/predbat/tests/test_ml_load_fallback.py @@ -0,0 +1,69 @@ +from tests.test_infra import TestHAInterface +from fetch import Fetch +from datetime import datetime, timezone + + +class TestFetchMLFallback(Fetch): + def __init__(self, ha_interface, base): + self.ha_interface = ha_interface + self.base = base + self.prefix = "predbat" + self.forecast_days = 2 + self.midnight_utc = datetime.now(timezone.utc) + self.minutes_now = 0 + self.queried_results = False + + def get_state_wrapper(self, entity_id, attribute=None, default=None): + if attribute is None: + if "inactive_test" in entity_id: + return "error" + else: + return "active" + if attribute == "results": + self.queried_results = True + # Return an empty dict so minute_data doesn't crash on mocked data + return {} + return default + + def log(self, msg): + pass + + +def run_ml_load_fallback_tests(my_predbat): + failed = False + print("\n============================================================") + print("Running ML Load Fallback tests") + print("============================================================") + + ha = TestHAInterface() + fetch = TestFetchMLFallback(ha, my_predbat) + now_utc = datetime.now(timezone.utc) + + # Test 1: Active status proceeds + fetch.prefix = "active_test" + fetch.queried_results = False + fetch.fetch_ml_load_forecast(now_utc) + if not fetch.queried_results: + print("FAIL: ML Load forecast did NOT query 'results' on active path") + failed = True + else: + print("PASS: ML Load forecast active status proceeds past early return") + + # Test 2: Inactive status falls back + fetch.prefix = "inactive_test" + fetch.queried_results = False + fetch.fetch_ml_load_forecast(now_utc) + if fetch.queried_results: + print("FAIL: ML Load forecast queried 'results' when inactive") + failed = True + else: + print("PASS: ML Load forecast ignored when inactive (fallback)") + + print("============================================================") + if failed: + print("FAIL: SOME TESTS FAILED") + else: + print("PASS: ALL TESTS PASSED") + print("============================================================") + + return failed diff --git a/apps/predbat/tests/test_octopus_day_night_rates.py b/apps/predbat/tests/test_octopus_day_night_rates.py index f875dad59..4d640d975 100644 --- a/apps/predbat/tests/test_octopus_day_night_rates.py +++ b/apps/predbat/tests/test_octopus_day_night_rates.py @@ -4,9 +4,9 @@ Tests for OctopusAPI.async_get_day_night_rates — verifies that the correct night-window is selected for each of the three tariff families: - 1. IOG TOU (INTELLI or IOG+TOU in tariff_code) → 23:30–05:30, cross_midnight=True - 2. GO / generic day-night (not E-2R-*) → 00:30–05:30, cross_midnight=False - 3. Economy 7 (E-2R-* tariff_code) → 00:30–07:30, cross_midnight=False + 1. IOG TOU (INTELLI or IOG+TOU in tariff_code) -> 23:30–05:30, cross_midnight=True + 2. GO / generic day-night (not E-2R-*) -> 00:30–05:30, cross_midnight=False + 3. Economy 7 (E-2R-* tariff_code) -> 00:30–07:30, cross_midnight=False """ import asyncio @@ -67,11 +67,11 @@ async def test_octopus_day_night_rates(my_predbat): Test async_get_day_night_rates for all three tariff window cases. Tests: - - Test 1: IOG TOU tariff → night window 23:30–05:30 (crosses midnight) - - Test 2: INTELLI tariff → same IOG 23:30–05:30 window - - Test 3: GO-style tariff (non E-2R-) → night window 00:30–05:30 - - Test 4: Economy 7 tariff (E-2R-*) → night window 00:30–07:30 - - Test 5: Missing rates → returns empty list + - Test 1: IOG TOU tariff -> night window 23:30–05:30 (crosses midnight) + - Test 2: INTELLI tariff -> same IOG 23:30–05:30 window + - Test 3: GO-style tariff (non E-2R-) -> night window 00:30–05:30 + - Test 4: Economy 7 tariff (E-2R-*) -> night window 00:30–07:30 + - Test 5: Missing rates -> returns empty list """ print("\n**** Running async_get_day_night_rates tests ****") failed = False @@ -81,7 +81,7 @@ async def test_octopus_day_night_rates(my_predbat): # ------------------------------------------------------------------ # Test 1: IOG TOU — night window must be 23:30–05:30, cross-midnight # ------------------------------------------------------------------ - print("\n*** Test 1: IOG TOU tariff → night 23:30–05:30 ***") + print("\n*** Test 1: IOG TOU tariff -> night 23:30–05:30 ***") api1 = _make_api(my_predbat, day_rate=29.14, night_rate=7.00) with patch.object(type(api1), "now_utc_exact", new_callable=PropertyMock) as mock_now: @@ -118,7 +118,7 @@ async def test_octopus_day_night_rates(my_predbat): # ------------------------------------------------------------------ # Test 2: INTELLI tariff — same IOG window # ------------------------------------------------------------------ - print("\n*** Test 2: INTELLI tariff → same IOG 23:30–05:30 window ***") + print("\n*** Test 2: INTELLI tariff -> same IOG 23:30–05:30 window ***") api2 = _make_api(my_predbat, day_rate=29.14, night_rate=7.00) with patch.object(type(api2), "now_utc_exact", new_callable=PropertyMock) as mock_now: @@ -137,7 +137,7 @@ async def test_octopus_day_night_rates(my_predbat): # ------------------------------------------------------------------ # Test 3: GO-style tariff (non E-2R-) — night 00:30–05:30 # ------------------------------------------------------------------ - print("\n*** Test 3: GO-style tariff → night 00:30–05:30 ***") + print("\n*** Test 3: GO-style tariff -> night 00:30–05:30 ***") api3 = _make_api(my_predbat, day_rate=24.0, night_rate=8.5) with patch.object(type(api3), "now_utc_exact", new_callable=PropertyMock) as mock_now: @@ -162,7 +162,7 @@ async def test_octopus_day_night_rates(my_predbat): # ------------------------------------------------------------------ # Test 4: Economy 7 (E-2R-*) — night 00:30–07:30 # ------------------------------------------------------------------ - print("\n*** Test 4: Economy 7 tariff → night 00:30–07:30 ***") + print("\n*** Test 4: Economy 7 tariff -> night 00:30–07:30 ***") api4 = _make_api(my_predbat, day_rate=20.0, night_rate=10.0) with patch.object(type(api4), "now_utc_exact", new_callable=PropertyMock) as mock_now: @@ -185,9 +185,9 @@ async def test_octopus_day_night_rates(my_predbat): print("PASS: Economy 7 night window is 00:30–07:30") # ------------------------------------------------------------------ - # Test 5: Missing day/night rate data → returns empty list + # Test 5: Missing day/night rate data -> returns empty list # ------------------------------------------------------------------ - print("\n*** Test 5: Missing rates → returns empty list ***") + print("\n*** Test 5: Missing rates -> returns empty list ***") api5 = OctopusAPI(my_predbat, key="test-key", account_id="test-account", automatic=False) api5.fetch_url_cached = AsyncMock(return_value=[]) @@ -208,7 +208,7 @@ async def test_octopus_day_night_rates(my_predbat): # of the older rate (32.11992, valid from 2025-11-24) because the API # returns results newest-first and the loop was keeping the last match. # ------------------------------------------------------------------ - print("\n*** Test 6: Multiple historical day rates (newest first) → most-recent rate selected ***") + print("\n*** Test 6: Multiple historical day rates (newest first) -> most-recent rate selected ***") _NOW6 = datetime(2026, 5, 16, 12, 0, 0, tzinfo=timezone.utc) api6 = OctopusAPI(my_predbat, key="test-key", account_id="test-account", automatic=False) @@ -263,9 +263,9 @@ async def mock_fetch6(url, **kwargs): # rate snapshotted at "now". # Setup: now = 2026-04-02 12:00 UTC (2 days after rate change at 2026-03-31 23:00 UTC) # Schedule starts 2 days back = 2026-03-31 00:30 UTC (eco7 night start) - # Slots starting before 2026-03-31 23:00 → old rates; slots from 2026-04-01 onwards → new rates + # Slots starting before 2026-03-31 23:00 -> old rates; slots from 2026-04-01 onwards -> new rates # ------------------------------------------------------------------ - print("\n*** Test 7: Rate change within schedule window → per-day rate lookup ***") + print("\n*** Test 7: Rate change within schedule window -> per-day rate lookup ***") _NOW7 = datetime(2026, 4, 2, 12, 0, 0, tzinfo=timezone.utc) api7 = OctopusAPI(my_predbat, key="test-key", account_id="test-account", automatic=False) @@ -292,8 +292,8 @@ async def mock_fetch7(url, **kwargs): mdata7 = await api7.async_get_day_night_rates(base_url, tariff_code="E-2R-OE-FIX-12M-25-11-24-J") # The rate change is 2026-03-31T23:00Z. - # Eco7 night starts at 00:30 and day at 07:30, both before 23:00 on 03-31 → old rates. - # From 2026-04-01 00:30 onwards → new rates. + # Eco7 night starts at 00:30 and day at 07:30, both before 23:00 on 03-31 -> old rates. + # From 2026-04-01 00:30 onwards -> new rates. rate_change_dt = datetime(2026, 3, 31, 23, 0, 0, tzinfo=timezone.utc) old_rates = {11.55, 32.11992} new_rates = {13.65, 28.61292} @@ -330,9 +330,9 @@ async def mock_fetch7(url, **kwargs): _TS = datetime(2026, 5, 16, 12, 0, 0, tzinfo=timezone.utc) api8 = OctopusAPI(my_predbat, key="test-key", account_id="test-account", automatic=False) - # Entry A: valid_from missing (epoch), valid_to in the future → should match - # Entry B: valid_from very recent but valid_to already expired → must NOT match - # Entry C: valid_from set, valid_to absent (forever) → should match and beats A + # Entry A: valid_from missing (epoch), valid_to in the future -> should match + # Entry B: valid_from very recent but valid_to already expired -> must NOT match + # Entry C: valid_from set, valid_to absent (forever) -> should match and beats A rates8 = [ {"value_inc_vat": 10.0, "valid_from": None, "valid_to": "2027-01-01T00:00:00+0000"}, {"value_inc_vat": 99.0, "valid_from": "2026-05-16T11:00:00+0000", "valid_to": "2026-05-16T11:30:00+0000"}, @@ -412,8 +412,8 @@ async def mock_fetch7(url, **kwargs): # ------------------------------------------------------------------ if failed: - print("\n**** ❌ async_get_day_night_rates tests FAILED ****") + print("\n**** ERROR: async_get_day_night_rates tests FAILED ****") else: - print("\n**** ✅ All async_get_day_night_rates tests PASSED ****") + print("\n**** PASS: All async_get_day_night_rates tests PASSED ****") return 1 if failed else 0 diff --git a/apps/predbat/tests/test_octopus_download_rates.py b/apps/predbat/tests/test_octopus_download_rates.py index 81a6836d8..bfce051dd 100644 --- a/apps/predbat/tests/test_octopus_download_rates.py +++ b/apps/predbat/tests/test_octopus_download_rates.py @@ -50,13 +50,13 @@ def test_octopus_download_rates(my_predbat): result = my_predbat.download_octopus_rates("https://api.octopus.energy/v1/products/AGILE-24-04-03/electricity-tariffs/E-1R-AGILE-24-04-03-A/standard-unit-rates/") if result != mock_rate_data: - print(f"✗ Test 1 failed - Expected rate data, got {result}") + print(f"FAIL: Test 1 failed - Expected rate data, got {result}") failed = True elif "https://api.octopus.energy/v1/products/AGILE-24-04-03/electricity-tariffs/E-1R-AGILE-24-04-03-A/standard-unit-rates/" not in my_predbat.octopus_url_cache: - print("✗ Test 1 failed - URL not in cache") + print("FAIL: Test 1 failed - URL not in cache") failed = True else: - print("✓ Test 1 passed - Single page download successful") + print("PASS: Test 1 passed - Single page download successful") # Test 2: Cache hit - returns cached data when fresh print("\nTest 2: Cache hit - returns cached data when fresh") @@ -77,13 +77,13 @@ def test_octopus_download_rates(my_predbat): result = my_predbat.download_octopus_rates(test_url) if result != cached_data: - print(f"✗ Test 2 failed - Expected cached data {cached_data}, got {result}") + print(f"FAIL: Test 2 failed - Expected cached data {cached_data}, got {result}") failed = True elif mock_download.called: - print("✗ Test 2 failed - download_octopus_rates_func should not be called") + print("FAIL: Test 2 failed - download_octopus_rates_func should not be called") failed = True else: - print("✓ Test 2 passed - Cache hit returns fresh data") + print("PASS: Test 2 passed - Cache hit returns fresh data") # Test 3: Cache miss - downloads when cache expired (>30 min) print("\nTest 3: Cache miss - downloads when cache expired") @@ -104,13 +104,13 @@ def test_octopus_download_rates(my_predbat): result = my_predbat.download_octopus_rates(test_url) if result != new_data: - print(f"✗ Test 3 failed - Expected new data {new_data}, got {result}") + print(f"FAIL: Test 3 failed - Expected new data {new_data}, got {result}") failed = True elif my_predbat.octopus_url_cache[test_url]["data"] != new_data: - print("✗ Test 3 failed - Cache not updated with new data") + print("FAIL: Test 3 failed - Cache not updated with new data") failed = True else: - print("✓ Test 3 passed - Cache miss triggers download") + print("PASS: Test 3 passed - Cache miss triggers download") # Test 4: Download failure - returns stale cache when available print("\nTest 4: Download failure - returns stale cache when available") @@ -129,10 +129,10 @@ def test_octopus_download_rates(my_predbat): result = my_predbat.download_octopus_rates(test_url) if result != cached_data: - print(f"✗ Test 4 failed - Expected stale cached data {cached_data}, got {result}") + print(f"FAIL: Test 4 failed - Expected stale cached data {cached_data}, got {result}") failed = True else: - print("✓ Test 4 passed - Download failure returns stale cache") + print("PASS: Test 4 passed - Download failure returns stale cache") # Test 4a: Cache invalidation - midnight crossing print("\nTest 4a: Cache invalidation - midnight crossing") @@ -155,16 +155,16 @@ def test_octopus_download_rates(my_predbat): result = my_predbat.download_octopus_rates(test_url) if result != new_data: - print(f"✗ Test 4a failed - Expected new data {new_data}, got {result}") + print(f"FAIL: Test 4a failed - Expected new data {new_data}, got {result}") failed = True elif not mock_download.called: - print("✗ Test 4a failed - download_octopus_rates_func should be called after midnight crossing") + print("FAIL: Test 4a failed - download_octopus_rates_func should be called after midnight crossing") failed = True elif my_predbat.octopus_url_cache[test_url]["midnight_utc"] != my_predbat.midnight_utc: - print(f"✗ Test 4a failed - Cache should be updated with new midnight_utc {my_predbat.midnight_utc}, got {my_predbat.octopus_url_cache[test_url]['midnight_utc']}") + print(f"FAIL: Test 4a failed - Cache should be updated with new midnight_utc {my_predbat.midnight_utc}, got {my_predbat.octopus_url_cache[test_url]['midnight_utc']}") failed = True else: - print("✓ Test 4a passed - Cache invalidated after midnight crossing") + print("PASS: Test 4a passed - Cache invalidated after midnight crossing") # Test 5: Download failure - raises ValueError when no cache available print("\nTest 5: Download failure - raises ValueError when no cache") @@ -174,10 +174,10 @@ def test_octopus_download_rates(my_predbat): with patch.object(my_predbat, 'download_octopus_rates_func', return_value={}): try: result = my_predbat.download_octopus_rates(test_url) - print("✗ Test 5 failed - Expected ValueError to be raised") + print("FAIL: Test 5 failed - Expected ValueError to be raised") failed = True except ValueError: - print("✓ Test 5 passed - ValueError raised when no cache available") + print("PASS: Test 5 passed - ValueError raised when no cache available") # Test 6: Retry mechanism - succeeds on second attempt print("\nTest 6: Retry mechanism - succeeds on second attempt") @@ -190,10 +190,10 @@ def test_octopus_download_rates(my_predbat): result = my_predbat.download_octopus_rates(test_url) if result != success_data: - print(f"✗ Test 6 failed - Expected success data {success_data}, got {result}") + print(f"FAIL: Test 6 failed - Expected success data {success_data}, got {result}") failed = True else: - print("✓ Test 6 passed - Retry mechanism works correctly") + print("PASS: Test 6 passed - Retry mechanism works correctly") # Test 7: download_octopus_rates_func - successful single page print("\nTest 7: download_octopus_rates_func - successful single page") @@ -220,10 +220,10 @@ def test_octopus_download_rates(my_predbat): # minute_data expands to every minute in the range, so check key points if result.get(0) != 10.5 or result.get(29) != 10.5 or result.get(30) != 12.0 or result.get(59) != 12.0: - print(f"✗ Test 7 failed - Expected rate data at key minutes, got {result.get(0)}, {result.get(29)}, {result.get(30)}, {result.get(59)}") + print(f"FAIL: Test 7 failed - Expected rate data at key minutes, got {result.get(0)}, {result.get(29)}, {result.get(30)}, {result.get(59)}") failed = True else: - print("✓ Test 7 passed - download_octopus_rates_func single page success") + print("PASS: Test 7 passed - download_octopus_rates_func single page success") # Test 8: download_octopus_rates_func - pagination (multiple pages) print("\nTest 8: download_octopus_rates_func - pagination") @@ -256,10 +256,10 @@ def test_octopus_download_rates(my_predbat): # Check that both pages were fetched and combined if result.get(0) != 10.5 or result.get(29) != 10.5 or result.get(30) != 12.0 or result.get(59) != 12.0: - print(f"✗ Test 8 failed - Expected combined rate data, got minute 0: {result.get(0)}, minute 29: {result.get(29)}, minute 30: {result.get(30)}, minute 59: {result.get(59)}") + print(f"FAIL: Test 8 failed - Expected combined rate data, got minute 0: {result.get(0)}, minute 29: {result.get(29)}, minute 30: {result.get(30)}, minute 59: {result.get(59)}") failed = True else: - print("✓ Test 8 passed - Pagination works correctly") + print("PASS: Test 8 passed - Pagination works correctly") # Test 9: download_octopus_rates_func - ConnectionError print("\nTest 9: download_octopus_rates_func - ConnectionError") @@ -271,10 +271,10 @@ def test_octopus_download_rates(my_predbat): result = my_predbat.download_octopus_rates_func(test_url) if result != {}: - print(f"✗ Test 9 failed - Expected empty dict, got {result}") + print(f"FAIL: Test 9 failed - Expected empty dict, got {result}") failed = True else: - print("✓ Test 9 passed - ConnectionError handled correctly") + print("PASS: Test 9 passed - ConnectionError handled correctly") # Test 10: download_octopus_rates_func - HTTP error status print("\nTest 10: download_octopus_rates_func - HTTP error status") @@ -289,10 +289,10 @@ def test_octopus_download_rates(my_predbat): result = my_predbat.download_octopus_rates_func(test_url) if result != {}: - print(f"✗ Test 10 failed - Expected empty dict, got {result}") + print(f"FAIL: Test 10 failed - Expected empty dict, got {result}") failed = True else: - print("✓ Test 10 passed - HTTP error status handled") + print("PASS: Test 10 passed - HTTP error status handled") # Test 11: download_octopus_rates_func - JSON decode error print("\nTest 11: download_octopus_rates_func - JSON decode error") @@ -308,13 +308,13 @@ def test_octopus_download_rates(my_predbat): result = my_predbat.download_octopus_rates_func(test_url) if result != {}: - print(f"✗ Test 11 failed - Expected empty dict, got {result}") + print(f"FAIL: Test 11 failed - Expected empty dict, got {result}") failed = True elif my_predbat.failures_total != 1: - print(f"✗ Test 11 failed - Expected failures_total=1, got {my_predbat.failures_total}") + print(f"FAIL: Test 11 failed - Expected failures_total=1, got {my_predbat.failures_total}") failed = True else: - print("✓ Test 11 passed - JSON decode error handled") + print("PASS: Test 11 passed - JSON decode error handled") # Test 12: download_octopus_rates_func - missing 'results' key print("\nTest 12: download_octopus_rates_func - missing 'results' key") @@ -330,10 +330,10 @@ def test_octopus_download_rates(my_predbat): result = my_predbat.download_octopus_rates_func(test_url) if result != {}: - print(f"✗ Test 12 failed - Expected empty dict, got {result}") + print(f"FAIL: Test 12 failed - Expected empty dict, got {result}") failed = True else: - print("✓ Test 12 passed - Missing 'results' key handled") + print("PASS: Test 12 passed - Missing 'results' key handled") if failed: print("\n=== Some download_octopus_rates tests FAILED ===") diff --git a/apps/predbat/tests/test_octopus_misc.py b/apps/predbat/tests/test_octopus_misc.py index e3460c886..407e3df24 100644 --- a/apps/predbat/tests/test_octopus_misc.py +++ b/apps/predbat/tests/test_octopus_misc.py @@ -29,9 +29,9 @@ async def test_octopus_misc(my_predbat): failed += await test_octopus_run(my_predbat) if failed == 0: - print("\n**** ✅ All Octopus Misc API tests PASSED ****") + print("\n**** PASS: All Octopus Misc API tests PASSED ****") else: - print(f"\n**** ❌ Octopus Misc API tests FAILED ({failed} test(s) failed) ****") + print(f"\n**** ERROR: Octopus Misc API tests FAILED ({failed} test(s) failed) ****") return failed @@ -273,10 +273,10 @@ def capture_log(msg): print("PASS: returns_data=False parameter set correctly") if failed: - print("\n**** ❌ Octopus async_set_intelligent_target_schedule tests FAILED ****") + print("\n**** ERROR: Octopus async_set_intelligent_target_schedule tests FAILED ****") return 1 else: - print("\n**** ✅ Octopus async_set_intelligent_target_schedule tests PASSED ****") + print("\n**** PASS: Octopus async_set_intelligent_target_schedule tests PASSED ****") return 0 @@ -442,10 +442,10 @@ def capture_log(msg): print("PASS: Multiple events can be joined sequentially") if failed: - print("\n**** ❌ Octopus async_join_saving_session_events tests FAILED ****") + print("\n**** ERROR: Octopus async_join_saving_session_events tests FAILED ****") return 1 else: - print("\n**** ✅ Octopus async_join_saving_session_events tests PASSED ****") + print("\n**** PASS: Octopus async_join_saving_session_events tests PASSED ****") return 0 @@ -613,10 +613,10 @@ async def test_octopus_get_saving_sessions(my_predbat): print("PASS: Returns correct savingSessions structure") if failed: - print("\n**** ❌ Octopus async_get_saving_sessions tests FAILED ****") + print("\n**** ERROR: Octopus async_get_saving_sessions tests FAILED ****") return 1 else: - print("\n**** ✅ Octopus async_get_saving_sessions tests PASSED ****") + print("\n**** PASS: Octopus async_get_saving_sessions tests PASSED ****") return 0 @@ -858,8 +858,8 @@ def capture_dashboard_item(entity_id, state, attributes=None, app=None): else: print("PASS: All dashboard_item calls use app='octopus'") - # Test 7: Product info returns day/night links → async_get_day_night_rates is used - print("\n*** Test 7: Product info with day_unit_rates links → routes to async_get_day_night_rates ***") + # Test 7: Product info returns day/night links -> async_get_day_night_rates is used + print("\n*** Test 7: Product info with day_unit_rates links -> routes to async_get_day_night_rates ***") api7 = OctopusAPI(my_predbat, key="test-api-key-7", account_id="test-account-7", automatic=False) iog_tariff_code = "E-1R-IOG-SMB-TOU-25-12-12-H" @@ -926,8 +926,8 @@ async def mock_fetch_url7(url, **kwargs): else: print("PASS: Day/night rate data stored correctly in tariff") - # Test 8: INTELLI-FLUX-EXPORT 404s → fetches FLUX-IMPORT as fallback - print("\n*** Test 8: INTELLI-FLUX-EXPORT 404s → fetches FLUX-IMPORT as fallback ***") + # Test 8: INTELLI-FLUX-EXPORT 404s -> fetches FLUX-IMPORT as fallback + print("\n*** Test 8: INTELLI-FLUX-EXPORT 404s -> fetches FLUX-IMPORT as fallback ***") api8 = OctopusAPI(my_predbat, key="test-api-key-8", account_id="test-account-8", automatic=False) flux_export_product = "INTELLI-FLUX-EXPORT-23-07-14" @@ -981,8 +981,8 @@ async def mock_fetch_url8(url, **kwargs): else: print("PASS: FLUX-IMPORT URL constructed correctly with correct product and tariff codes") - # Test 9: INTELLI-FLUX-EXPORT 404s, FLUX-IMPORT also 404s → falls back to current import data - print("\n*** Test 9: FLUX-EXPORT and FLUX-IMPORT both fail → falls back to current import data ***") + # Test 9: INTELLI-FLUX-EXPORT 404s, FLUX-IMPORT also 404s -> falls back to current import data + print("\n*** Test 9: FLUX-EXPORT and FLUX-IMPORT both fail -> falls back to current import data ***") api9 = OctopusAPI(my_predbat, key="test-api-key-9", account_id="test-account-9", automatic=False) fix_import_rates = [{"valid_from": "2025-01-01T00:00:00Z", "valid_to": "2025-01-01T00:30:00Z", "value_inc_vat": 28.14}] @@ -1012,10 +1012,10 @@ async def mock_fetch_url9(url, **kwargs): print("PASS: Current import rates used as final fallback when FLUX-IMPORT also unavailable") if failed: - print("\n**** ❌ Octopus fetch_tariffs tests FAILED ****") + print("\n**** ERROR: Octopus fetch_tariffs tests FAILED ****") return 1 else: - print("\n**** ✅ Octopus fetch_tariffs tests PASSED ****") + print("\n**** PASS: Octopus fetch_tariffs tests PASSED ****") return 0 @@ -1245,10 +1245,10 @@ def capture_log(msg): print(f"PASS: Result covers minutes {min_minute} to {max_minute}") if failed: - print("\n**** ❌ Octopus get_octopus_rates_direct tests FAILED ****") + print("\n**** ERROR: Octopus get_octopus_rates_direct tests FAILED ****") return 1 else: - print("\n**** ✅ Octopus get_octopus_rates_direct tests PASSED ****") + print("\n**** PASS: Octopus get_octopus_rates_direct tests PASSED ****") return 0 @@ -1397,10 +1397,10 @@ def test_octopus_get_intelligent_target_soc(my_predbat): print("PASS: Returns None when weekend_target_soc missing") if failed: - print("\n**** ❌ Octopus get_intelligent_target_soc tests FAILED ****") + print("\n**** ERROR: Octopus get_intelligent_target_soc tests FAILED ****") return 1 else: - print("\n**** ✅ Octopus get_intelligent_target_soc tests PASSED ****") + print("\n**** PASS: Octopus get_intelligent_target_soc tests PASSED ****") return 0 @@ -1547,10 +1547,10 @@ def test_octopus_get_intelligent_target_time(my_predbat): print("PASS: Returns None when weekend_target_time missing") if failed: - print("\n**** ❌ Octopus get_intelligent_target_time tests FAILED ****") + print("\n**** ERROR: Octopus get_intelligent_target_time tests FAILED ****") return 1 else: - print("\n**** ✅ Octopus get_intelligent_target_time tests PASSED ****") + print("\n**** PASS: Octopus get_intelligent_target_time tests PASSED ****") return 0 @@ -1649,10 +1649,10 @@ def test_octopus_get_intelligent_battery_size(my_predbat): print("PASS: Float battery size retrieved correctly") if failed: - print("\n**** ❌ Octopus get_intelligent_battery_size tests FAILED ****") + print("\n**** ERROR: Octopus get_intelligent_battery_size tests FAILED ****") return 1 else: - print("\n**** ✅ Octopus get_intelligent_battery_size tests PASSED ****") + print("\n**** PASS: Octopus get_intelligent_battery_size tests PASSED ****") return 0 @@ -1845,10 +1845,10 @@ def test_octopus_get_intelligent_vehicle(my_predbat): print("PASS: Returns empty dict when device has no vehicle fields") if failed: - print("\n**** ❌ Octopus get_intelligent_vehicle tests FAILED ****") + print("\n**** ERROR: Octopus get_intelligent_vehicle tests FAILED ****") return 1 else: - print("\n**** ✅ Octopus get_intelligent_vehicle tests PASSED ****") + print("\n**** PASS: Octopus get_intelligent_vehicle tests PASSED ****") return 0 @@ -1888,7 +1888,7 @@ async def test_octopus_run(my_predbat): # Test 1: First run with no cache — all methods should be called print("\n*** Test 1: First run calls all expected methods ***") api = _mock_run_api(my_predbat, key="test-api-key", account_id="test-account") - # tariff_fetched_at and device_fetched_at are None → _data_age_minutes returns 9999 + # tariff_fetched_at and device_fetched_at are None -> _data_age_minutes returns 9999 result = await api.run(seconds=0, first=True) @@ -2095,8 +2095,8 @@ async def test_octopus_run(my_predbat): print("PASS: Automatic config called on first run when automatic=True") if failed: - print("\n**** ❌ Octopus run method tests FAILED ****") + print("\n**** ERROR: Octopus run method tests FAILED ****") return 1 else: - print("\n**** ✅ Octopus run method tests PASSED ****") + print("\n**** PASS: Octopus run method tests PASSED ****") return 0 diff --git a/apps/predbat/tests/test_octopus_read_response_retry.py b/apps/predbat/tests/test_octopus_read_response_retry.py index eae6a8341..afea80724 100644 --- a/apps/predbat/tests/test_octopus_read_response_retry.py +++ b/apps/predbat/tests/test_octopus_read_response_retry.py @@ -267,8 +267,8 @@ def create_mock_response(status, text_content, headers=None): print("PASS: Auth error triggers retry with exponential backoff") if failed: - print("\n**** ❌ Octopus async_read_response_retry tests FAILED ****") + print("\n**** ERROR: Octopus async_read_response_retry tests FAILED ****") return 1 else: - print("\n**** ✅ All Octopus async_read_response_retry tests PASSED ****") + print("\n**** PASS: All Octopus async_read_response_retry tests PASSED ****") return 0 diff --git a/apps/predbat/tests/test_octopus_url.py b/apps/predbat/tests/test_octopus_url.py index da33935c4..a3d909ecc 100644 --- a/apps/predbat/tests/test_octopus_url.py +++ b/apps/predbat/tests/test_octopus_url.py @@ -54,13 +54,13 @@ def test_octopus_url(my_predbat=None): try: result = test_func(my_predbat) if result: - print(f"✗ FAILED: {key}") + print(f"FAIL: FAILED: {key}") failed += 1 else: - print(f"✓ PASSED: {key}") + print(f"PASS: PASSED: {key}") passed += 1 except Exception as e: - print(f"✗ EXCEPTION in {key}: {e}") + print(f"FAIL: EXCEPTION in {key}: {e}") import traceback traceback.print_exc() diff --git a/apps/predbat/tests/test_ohme.py b/apps/predbat/tests/test_ohme.py index c2547448e..ec2dcdb03 100644 --- a/apps/predbat/tests/test_ohme.py +++ b/apps/predbat/tests/test_ohme.py @@ -291,13 +291,13 @@ def test_ohme(my_predbat=None): try: result = test_func(my_predbat) if result: - print(f"✗ FAILED: {key}") + print(f"FAIL: FAILED: {key}") failed += 1 else: - print(f"✓ PASSED: {key}") + print(f"PASS: PASSED: {key}") passed += 1 except Exception as e: - print(f"✗ EXCEPTION in {key}: {e}") + print(f"FAIL: EXCEPTION in {key}: {e}") import traceback traceback.print_exc() failed += 1 diff --git a/apps/predbat/tests/test_open_meteo.py b/apps/predbat/tests/test_open_meteo.py index fb9bec7c6..b0fe5f4cf 100644 --- a/apps/predbat/tests/test_open_meteo.py +++ b/apps/predbat/tests/test_open_meteo.py @@ -58,7 +58,7 @@ def _make_ensemble_response(times=None, members=None): def test_ensemble_returns_p10_values(my_predbat): """ - download_open_meteo_ensemble_data should return a dict of ts→kW10 + download_open_meteo_ensemble_data should return a dict of ts->kW10 where each value is the 10th-percentile GTI across members, converted to kW. """ print(" - test_ensemble_returns_p10_values") @@ -133,7 +133,7 @@ def test_ensemble_empty_on_http_failure(my_predbat): test_api = create_test_solar_api() try: test_api.solar.open_meteo_forecast_max_age = 1.0 - # No mock response registered → cache_get_url will return None + # No mock response registered -> cache_get_url will return None def create_mock_session(*args, **kwargs): return test_api.mock_aiohttp_session() @@ -235,7 +235,7 @@ def test_download_open_meteo_data_temperature_derating(my_predbat): test_api.solar.open_meteo_forecast_max_age = 1.0 # GTI=1000, T=45°C, wind=1.0 m/s: T_cell via SAPM model, eta = 1 - 0.004*(T_cell-25) - # pv50 = (1000/1000) * 4.0 * eta_temp (same at both ends → trapz average equals point value) + # pv50 = (1000/1000) * 4.0 * eta_temp (same at both ends -> trapz average equals point value) forecast_response = _make_forecast_response(times=["2025-06-15T12:00", "2025-06-15T13:00"], gti=[1000.0, 1000.0], temp=[45.0, 45.0], wind=[1.0, 1.0]) ensemble_response = _make_ensemble_response(times=["2025-06-15T12:00", "2025-06-15T13:00"], members={"global_tilted_irradiance_member01": [900.0, 900.0]}) test_api.set_mock_response("api.open-meteo.com", forecast_response) @@ -273,7 +273,7 @@ def test_download_open_meteo_data_multi_config(my_predbat): test_api = create_test_solar_api() try: - # Two identical 2 kWp arrays → combined should be 4 kWp + # Two identical 2 kWp arrays -> combined should be 4 kWp test_api.solar.open_meteo_forecast = [ {"latitude": 51.5, "longitude": -0.1, "declination": 35, "azimuth": 180, "kwp": 2.0, "efficiency": 1.0}, {"latitude": 51.5, "longitude": -0.1, "declination": 35, "azimuth": 180, "kwp": 2.0, "efficiency": 1.0}, @@ -295,8 +295,8 @@ def create_mock_session(*args, **kwargs): print(f"ERROR: max_kwh expected 4.0, got {max_kwh}") failed = True - # Each array: GTI=1000, T=25°C, wind=1.0 m/s → cell temp via SAPM, pv50 = (1000/1000)*2.0*eta - # Same at both ends → trapz average equals point value + # Each array: GTI=1000, T=25°C, wind=1.0 m/s -> cell temp via SAPM, pv50 = (1000/1000)*2.0*eta + # Same at both ends -> trapz average equals point value if sorted_data: pv50 = sorted_data[0].get("pv_estimate", 0) t_cell = pvwatts_cell_temperature(1000.0, 25.0, 1.0) @@ -369,7 +369,7 @@ def test_download_open_meteo_data_cool_temp_efficiency(my_predbat): test_api.solar.open_meteo_forecast_max_age = 1.0 # 10 degC ambient, 200 W/m2, 1 m/s wind: SAPM T_cell < 25 degC -> eta > 1.0 - # Same at both ends → trapz average equals point value + # Same at both ends -> trapz average equals point value forecast_response = _make_forecast_response(times=["2025-04-15T12:00", "2025-04-15T13:00"], gti=[200.0, 200.0], temp=[10.0, 10.0], wind=[1.0, 1.0]) ensemble_response = _make_ensemble_response(times=["2025-04-15T12:00", "2025-04-15T13:00"], members={"global_tilted_irradiance_member01": [150.0, 150.0]}) test_api.set_mock_response("api.open-meteo.com", forecast_response) @@ -456,8 +456,8 @@ def test_download_open_meteo_data_two_aspect_configs(my_predbat): test_api = create_test_solar_api() try: - # Array 1: WSW-facing, shallow tilt (az -133 Solcast → convert_azimuth → -47 OM) - # Array 2: NW-facing, steep tilt (az +45 Solcast → convert_azimuth → 135 OM) + # Array 1: WSW-facing, shallow tilt (az -133 Solcast -> convert_azimuth -> -47 OM) + # Array 2: NW-facing, steep tilt (az +45 Solcast -> convert_azimuth -> 135 OM) test_api.solar.open_meteo_forecast = [ {"latitude": 51.49, "longitude": -2.49, "declination": 23.0, "azimuth": -133.0, "kwp": 1.56, "efficiency": 1.0}, {"latitude": 51.49, "longitude": -2.49, "declination": 45.0, "azimuth": 45.0, "kwp": 2.73, "efficiency": 1.0}, @@ -465,7 +465,7 @@ def test_download_open_meteo_data_two_aspect_configs(my_predbat): test_api.solar.open_meteo_forecast_max_age = 1.0 times = ["2025-06-15T12:00", "2025-06-15T13:00"] - # Array 1 (WSW): 400 W/m² Array 2 (NW): 200 W/m² (same at both ends → trapz average = point value) + # Array 1 (WSW): 400 W/m² Array 2 (NW): 200 W/m² (same at both ends -> trapz average = point value) forecast_wsw = {"hourly": {"time": times, "global_tilted_irradiance": [400.0, 400.0], "temperature_2m": [25.0, 25.0], "wind_speed_10m": [1.0, 1.0]}} forecast_nw = {"hourly": {"time": times, "global_tilted_irradiance": [200.0, 200.0], "temperature_2m": [25.0, 25.0], "wind_speed_10m": [1.0, 1.0]}} ensemble_wsw = {"hourly": {"time": times, "global_tilted_irradiance_member01": [320.0, 320.0]}} @@ -542,7 +542,7 @@ def test_download_open_meteo_data_http_failure(my_predbat): try: test_api.solar.open_meteo_forecast = [{"latitude": 51.5, "longitude": -0.1, "declination": 35, "azimuth": 180, "kwp": 3.0, "efficiency": 0.86}] test_api.solar.open_meteo_forecast_max_age = 1.0 - # No mocks set → cache_get_url returns None + # No mocks set -> cache_get_url returns None def create_mock_session(*args, **kwargs): return test_api.mock_aiohttp_session() @@ -714,7 +714,7 @@ def create_mock_session(*args, **kwargs): test_api.cleanup() # --- Case 2: azimuth_zero_south=False (default), azimuth=0 (North in Predbat convention) --- - # convert_azimuth(0) → 180; URL should contain azimuth=180 + # convert_azimuth(0) -> 180; URL should contain azimuth=180 test_api = create_test_solar_api() try: test_api.solar.open_meteo_forecast = [{"latitude": 51.5, "longitude": -0.1, "declination": 35, "azimuth": 0, "kwp": 3.0, "efficiency": 1.0}] diff --git a/apps/predbat/tests/test_rate_add_io_slots.py b/apps/predbat/tests/test_rate_add_io_slots.py index abac7bbf4..e3cd93baa 100644 --- a/apps/predbat/tests/test_rate_add_io_slots.py +++ b/apps/predbat/tests/test_rate_add_io_slots.py @@ -271,7 +271,7 @@ def run_rate_add_io_slots_tests(my_predbat): # Test 15: Midday-to-midday cap boundary # 14 slots starting at 22:00 today (minute 1320) and crossing midnight into the early hours of tomorrow. - # All slots fall within the same midday-to-midday period (noon today → noon tomorrow), so the + # All slots fall within the same midday-to-midday period (noon today -> noon tomorrow), so the # 12-slot cap applies across midnight and only the first 12 slots should be cheap. # Under the old midnight-to-midnight logic, today would have 4 cheap slots and tomorrow 10 # cheap slots (each under the limit), so ALL 14 would be cheap — the opposite of what we want. diff --git a/apps/predbat/tests/test_rate_min_forward_calc.py b/apps/predbat/tests/test_rate_min_forward_calc.py index 7d399eb64..114dbd925 100644 --- a/apps/predbat/tests/test_rate_min_forward_calc.py +++ b/apps/predbat/tests/test_rate_min_forward_calc.py @@ -49,7 +49,7 @@ def test_rate_min_forward_calc(my_predbat): result = my_predbat.rate_min_forward_calc(rates) failed |= _check_range(result, output_start, output_end, 10.0, "flat rate") - # --- Test 2: lower rate in the future → every minute sees that future minimum --- + # --- Test 2: lower rate in the future -> every minute sees that future minimum --- print("*** rate_min_forward_calc test 2: lower rate in future") low_minute = my_predbat.minutes_now + 6 * 60 # 6 hours from now rates = {m: 10.0 for m in range(0, my_predbat.forecast_minutes + my_predbat.minutes_now + 48 * 60)} @@ -64,7 +64,7 @@ def test_rate_min_forward_calc(my_predbat): # After low_minute back to 10.0 failed |= _check_range(result, low_minute + 1, output_end, 10.0, "after low_minute") - # --- Test 3: rates strictly increasing → min forward equals the rate at each minute --- + # --- Test 3: rates strictly increasing -> min forward equals the rate at each minute --- print("*** rate_min_forward_calc test 3: increasing rates") rates = {} for m in range(my_predbat.forecast_minutes + my_predbat.minutes_now + 48 * 60): diff --git a/apps/predbat/tests/test_rate_replicate_missing_slots.py b/apps/predbat/tests/test_rate_replicate_missing_slots.py index b02520525..6726d20c3 100644 --- a/apps/predbat/tests/test_rate_replicate_missing_slots.py +++ b/apps/predbat/tests/test_rate_replicate_missing_slots.py @@ -53,13 +53,13 @@ def test_rate_replicate(my_predbat): try: test_result = test_func(my_predbat) if test_result: - print(f"✗ FAILED: {test_name}") + print(f"FAIL: FAILED: {test_name}") failed += 1 else: - print(f"✓ PASSED: {test_name}") + print(f"PASS: PASSED: {test_name}") passed += 1 except Exception as e: - print(f"✗ EXCEPTION in {test_name}: {e}") + print(f"FAIL: EXCEPTION in {test_name}: {e}") import traceback traceback.print_exc() failed += 1 @@ -128,32 +128,32 @@ def _test_missing_slots(my_predbat): # Expected behavior: 23:00 and 23:30 should be replicated from previous day if 1380 not in result: - print(f" ✗ ERROR: Minute 1380 (23:00) is still missing after rate_replicate") + print(f" FAIL: ERROR: Minute 1380 (23:00) is still missing after rate_replicate") failed |= 1 elif result[1380] != rates[-60]: - print(f" ✗ ERROR: Minute 1380 (23:00) should be {rates[-60]} (from minute -60), got {result[1380]}") + print(f" FAIL: ERROR: Minute 1380 (23:00) should be {rates[-60]} (from minute -60), got {result[1380]}") failed |= 1 elif result[1380] == 0.0: - print(f" ✗ ERROR: Minute 1380 (23:00) is 0.0 - this is the BUG we're fixing!") + print(f" FAIL: ERROR: Minute 1380 (23:00) is 0.0 - this is the BUG we're fixing!") failed |= 1 if 1410 not in result: - print(f" ✗ ERROR: Minute 1410 (23:30) is still missing after rate_replicate") + print(f" FAIL: ERROR: Minute 1410 (23:30) is still missing after rate_replicate") failed |= 1 elif result[1410] != rates[-30]: - print(f" ✗ ERROR: Minute 1410 (23:30) should be {rates[-30]} (from minute -30), got {result[1410]}") + print(f" FAIL: ERROR: Minute 1410 (23:30) should be {rates[-30]} (from minute -30), got {result[1410]}") failed |= 1 elif result[1410] == 0.0: - print(f" ✗ ERROR: Minute 1410 (23:30) is 0.0 - this is the BUG we're fixing!") + print(f" FAIL: ERROR: Minute 1410 (23:30) is 0.0 - this is the BUG we're fixing!") failed |= 1 # Check that replicated type is marked as "copy" if result_replicated.get(1380) != "copy": - print(f" ✗ ERROR: Minute 1380 should be marked as 'copy', got {result_replicated.get(1380)}") + print(f" FAIL: ERROR: Minute 1380 should be marked as 'copy', got {result_replicated.get(1380)}") failed |= 1 if result_replicated.get(1410) != "copy": - print(f" ✗ ERROR: Minute 1410 should be marked as 'copy', got {result_replicated.get(1410)}") + print(f" FAIL: ERROR: Minute 1410 should be marked as 'copy', got {result_replicated.get(1410)}") failed |= 1 # Restore time context to current time my_predbat.now_utc = datetime.now(my_predbat.local_tz) @@ -190,17 +190,17 @@ def _test_no_previous_day(my_predbat): # Without previous day data, the function should fall back to rate_last (last seen rate) # which should be rates[1350] = 15.0 (22:30's rate) if 1380 not in result: - print(f" ✗ ERROR: Minute 1380 should be filled (even without previous day data)") + print(f" FAIL: ERROR: Minute 1380 should be filled (even without previous day data)") failed |= 1 elif result[1380] == 0.0: - print(f" ✗ ERROR: Minute 1380 should not be 0.0 (should use rate_last fallback)") + print(f" FAIL: ERROR: Minute 1380 should not be 0.0 (should use rate_last fallback)") failed |= 1 if 1410 not in result: - print(f" ✗ ERROR: Minute 1410 should be filled (even without previous day data)") + print(f" FAIL: ERROR: Minute 1410 should be filled (even without previous day data)") failed |= 1 elif result[1410] == 0.0: - print(f" ✗ ERROR: Minute 1410 should not be 0.0 (should use rate_last fallback)") + print(f" FAIL: ERROR: Minute 1410 should not be 0.0 (should use rate_last fallback)") failed |= 1 # Restore time context to current time @@ -250,22 +250,22 @@ def _test_zero_rates(my_predbat): # Expected: 23:00 and 23:30 should be replicated from previous day's 0.0 rates if 1380 not in result: - print(f" ✗ ERROR: Minute 1380 (23:00) is missing after rate_replicate") + print(f" FAIL: ERROR: Minute 1380 (23:00) is missing after rate_replicate") failed |= 1 elif result[1380] != 0.0: - print(f" ✗ ERROR: Minute 1380 (23:00) should be 0.0 (from minute -60), got {result[1380]}") + print(f" FAIL: ERROR: Minute 1380 (23:00) should be 0.0 (from minute -60), got {result[1380]}") failed |= 1 if 1410 not in result: - print(f" ✗ ERROR: Minute 1410 (23:30) is missing after rate_replicate") + print(f" FAIL: ERROR: Minute 1410 (23:30) is missing after rate_replicate") failed |= 1 elif result[1410] != 0.0: - print(f" ✗ ERROR: Minute 1410 (23:30) should be 0.0 (from minute -30), got {result[1410]}") + print(f" FAIL: ERROR: Minute 1410 (23:30) should be 0.0 (from minute -30), got {result[1410]}") failed |= 1 # Also verify that previous day rates with 0 are still present if result.get(-60) != 0.0: - print(f" ✗ ERROR: Previous day minute -60 should still be 0.0, got {result.get(-60)}") + print(f" FAIL: ERROR: Previous day minute -60 should still be 0.0, got {result.get(-60)}") failed |= 1 # Restore time context to current time my_predbat.now_utc = datetime.now(my_predbat.local_tz) @@ -317,7 +317,7 @@ def _test_undefined_negative_minutes(my_predbat): missing_minutes.append(minute) if missing_minutes: - print(f" ✗ ERROR: {len(missing_minutes)} negative minutes are undefined after rate_replicate") + print(f" FAIL: ERROR: {len(missing_minutes)} negative minutes are undefined after rate_replicate") print(f" First missing: {missing_minutes[0]}, Last missing: {missing_minutes[-1]}") print(f" This would cause KeyError in publish_rates when iterating from -1440") @@ -328,17 +328,17 @@ def _test_undefined_negative_minutes(my_predbat): value = result[test_minute] # This will raise KeyError print(f" No KeyError at minute {test_minute} (unexpected!)") except KeyError as e: - print(f" ✓ Confirmed: KeyError accessing minute {test_minute}: {e}") + print(f" PASS: Confirmed: KeyError accessing minute {test_minute}: {e}") failed |= 1 else: - print(f" ✓ All negative minutes were filled correctly") + print(f" PASS: All negative minutes were filled correctly") # Also check if any positive minutes were created (there shouldn't be any with only negative input) positive_minutes = [m for m in result.keys() if m >= 0] if positive_minutes: print(f" Note: {len(positive_minutes)} positive minutes were created despite no input data") else: - print(f" ✓ No positive minutes created (expected with only negative input)") + print(f" PASS: No positive minutes created (expected with only negative input)") # Restore time context to current time my_predbat.now_utc = datetime.now(my_predbat.local_tz) @@ -383,20 +383,20 @@ def _test_rate_io(my_predbat): next_day_intelligent_start = 1440 + 120 if next_day_intelligent_start in result: if result[next_day_intelligent_start] == my_predbat.rate_max: - print(f" ✓ Intelligent slot correctly replaced with rate_max: {result[next_day_intelligent_start]}") + print(f" PASS: Intelligent slot correctly replaced with rate_max: {result[next_day_intelligent_start]}") else: - print(f" ✗ ERROR: Next day intelligent slot should be {my_predbat.rate_max}, got {result[next_day_intelligent_start]}") + print(f" FAIL: ERROR: Next day intelligent slot should be {my_predbat.rate_max}, got {result[next_day_intelligent_start]}") failed |= 1 else: - print(f" ✗ ERROR: Next day intelligent slot minute {next_day_intelligent_start} missing") + print(f" FAIL: ERROR: Next day intelligent slot minute {next_day_intelligent_start} missing") failed |= 1 # Normal rates should still replicate correctly normal_minute_next_day = 1440 + 600 # 10:00 next day if normal_minute_next_day in result and result[normal_minute_next_day] == 25.0: - print(f" ✓ Normal rates replicate correctly: {result[normal_minute_next_day]}") + print(f" PASS: Normal rates replicate correctly: {result[normal_minute_next_day]}") else: - print(f" ✗ ERROR: Normal rate replication failed at minute {normal_minute_next_day}") + print(f" FAIL: ERROR: Normal rate replication failed at minute {normal_minute_next_day}") failed |= 1 # Restore context @@ -443,17 +443,17 @@ def _test_future_rate_adjust_import(my_predbat): next_day_minute = 1440 + 600 if next_day_minute in result: if result[next_day_minute] == 20.0: - print(f" ✓ Future rate adjustment applied: {result[next_day_minute]}") + print(f" PASS: Future rate adjustment applied: {result[next_day_minute]}") if result_replicated.get(next_day_minute) == "future": - print(f" ✓ Replicated type correctly marked as 'future'") + print(f" PASS: Replicated type correctly marked as 'future'") else: - print(f" ✗ ERROR: Should be marked as 'future', got {result_replicated.get(next_day_minute)}") + print(f" FAIL: ERROR: Should be marked as 'future', got {result_replicated.get(next_day_minute)}") failed |= 1 else: - print(f" ✗ ERROR: Future rate should be 20.0, got {result[next_day_minute]}") + print(f" FAIL: ERROR: Future rate should be 20.0, got {result[next_day_minute]}") failed |= 1 else: - print(f" ✗ ERROR: Future rate minute {next_day_minute} missing") + print(f" FAIL: ERROR: Future rate minute {next_day_minute} missing") failed |= 1 # Restore context @@ -502,17 +502,17 @@ def _test_future_rate_adjust_export(my_predbat): next_day_minute = 1440 + 600 if next_day_minute in result: if result[next_day_minute] == 0.0: - print(f" ✓ Negative export rate clamped to 0: {result[next_day_minute]}") + print(f" PASS: Negative export rate clamped to 0: {result[next_day_minute]}") if result_replicated.get(next_day_minute) == "future": - print(f" ✓ Replicated type correctly marked as 'future'") + print(f" PASS: Replicated type correctly marked as 'future'") else: - print(f" ✗ ERROR: Should be marked as 'future', got {result_replicated.get(next_day_minute)}") + print(f" FAIL: ERROR: Should be marked as 'future', got {result_replicated.get(next_day_minute)}") failed |= 1 else: - print(f" ✗ ERROR: Negative export rate should be clamped to 0.0, got {result[next_day_minute]}") + print(f" FAIL: ERROR: Negative export rate should be clamped to 0.0, got {result[next_day_minute]}") failed |= 1 else: - print(f" ✗ ERROR: Future rate minute {next_day_minute} missing") + print(f" FAIL: ERROR: Future rate minute {next_day_minute} missing") failed |= 1 # Restore context @@ -553,21 +553,21 @@ def _test_import_offset(my_predbat): if next_day_minute in result: expected_rate = 15.0 + 5.0 if result[next_day_minute] == expected_rate: - print(f" ✓ Import offset applied correctly: {result[next_day_minute]}") + print(f" PASS: Import offset applied correctly: {result[next_day_minute]}") if result_replicated.get(next_day_minute) == "offset": - print(f" ✓ Replicated type correctly marked as 'offset'") + print(f" PASS: Replicated type correctly marked as 'offset'") else: - print(f" ✗ ERROR: Should be marked as 'offset', got {result_replicated.get(next_day_minute)}") + print(f" FAIL: ERROR: Should be marked as 'offset', got {result_replicated.get(next_day_minute)}") failed |= 1 else: - print(f" ✗ ERROR: Rate should be {expected_rate}, got {result[next_day_minute]}") + print(f" FAIL: ERROR: Rate should be {expected_rate}, got {result[next_day_minute]}") failed |= 1 # Current day should not have offset if result[600] == 15.0: - print(f" ✓ Current day rate unchanged: {result[600]}") + print(f" PASS: Current day rate unchanged: {result[600]}") else: - print(f" ✗ ERROR: Current day should be 15.0, got {result[600]}") + print(f" FAIL: ERROR: Current day should be 15.0, got {result[600]}") failed |= 1 # Restore context @@ -606,14 +606,14 @@ def _test_export_offset_negative(my_predbat): next_day_minute = 1440 + 600 if next_day_minute in result: if result[next_day_minute] == 0.0: - print(f" ✓ Export rate with offset clamped to 0: {result[next_day_minute]}") + print(f" PASS: Export rate with offset clamped to 0: {result[next_day_minute]}") if result_replicated.get(next_day_minute) == "offset": - print(f" ✓ Replicated type correctly marked as 'offset'") + print(f" PASS: Replicated type correctly marked as 'offset'") else: - print(f" ✗ ERROR: Should be marked as 'offset', got {result_replicated.get(next_day_minute)}") + print(f" FAIL: ERROR: Should be marked as 'offset', got {result_replicated.get(next_day_minute)}") failed |= 1 else: - print(f" ✗ ERROR: Rate should be 0.0 (clamped), got {result[next_day_minute]}") + print(f" FAIL: ERROR: Rate should be 0.0 (clamped), got {result[next_day_minute]}") failed |= 1 # Test with higher rate that doesn't go negative @@ -624,9 +624,9 @@ def _test_export_offset_negative(my_predbat): result2, result_replicated2 = my_predbat.rate_replicate(rates2, is_import=False, is_gas=False) expected_rate = 15.0 - 8.0 if next_day_minute in result2 and result2[next_day_minute] == expected_rate: - print(f" ✓ Export rate with offset (not clamped): {result2[next_day_minute]}") + print(f" PASS: Export rate with offset (not clamped): {result2[next_day_minute]}") else: - print(f" ✗ ERROR: Rate should be {expected_rate}, got {result2.get(next_day_minute)}") + print(f" FAIL: ERROR: Rate should be {expected_rate}, got {result2.get(next_day_minute)}") failed |= 1 # Restore context @@ -667,9 +667,9 @@ def _test_gas_rates(my_predbat): expected_rate = 10.0 + 3.0 # Import offset is applied if next_day_minute in result: if result[next_day_minute] == expected_rate: - print(f" ✓ Gas import rates get import offset applied: {result[next_day_minute]}") + print(f" PASS: Gas import rates get import offset applied: {result[next_day_minute]}") else: - print(f" ✗ ERROR: Gas rate should be {expected_rate} (with import offset), got {result[next_day_minute]}") + print(f" FAIL: ERROR: Gas rate should be {expected_rate} (with import offset), got {result[next_day_minute]}") failed |= 1 # Test export gas rates - should NOT get export offset due to is_gas check @@ -682,9 +682,9 @@ def _test_gas_rates(my_predbat): # Export gas rates should replicate WITHOUT export offset (is_gas bypasses export offset) if next_day_minute in result2: if result2[next_day_minute] == 10.0: - print(f" ✓ Gas export rates replicate without export offset: {result2[next_day_minute]}") + print(f" PASS: Gas export rates replicate without export offset: {result2[next_day_minute]}") else: - print(f" ✗ ERROR: Gas export rate should be 10.0 (no export offset due to is_gas), got {result2[next_day_minute]}") + print(f" FAIL: ERROR: Gas export rate should be 10.0 (no export offset due to is_gas), got {result2[next_day_minute]}") failed |= 1 # Restore context diff --git a/apps/predbat/tests/test_sigenergy.py b/apps/predbat/tests/test_sigenergy.py index 351012f51..651276668 100644 --- a/apps/predbat/tests/test_sigenergy.py +++ b/apps/predbat/tests/test_sigenergy.py @@ -142,7 +142,7 @@ def __init__(self, prefix="predbat"): # ComponentBase attributes not set by initialize() — wire them manually self.api_started = False self.api_stop = False - # Skip mode-switch → command delay in unit tests + # Skip mode-switch -> command delay in unit tests self._command_delay = 0 # ComponentBase.storage looks at self.base.components, which this mock doesn't set up — # override it directly so tests can plug in a FakeStorage via self._mock_storage. @@ -204,15 +204,15 @@ def test_sigenergy_helper_functions(my_predbat): # _safe_float assert _safe_float(3.14) == 3.14, "_safe_float: float passthrough" assert _safe_float("2.5") == 2.5, "_safe_float: string to float" - assert _safe_float(None) == 0.0, "_safe_float: None → 0.0" - assert _safe_float("abc") == 0.0, "_safe_float: invalid string → 0.0" + assert _safe_float(None) == 0.0, "_safe_float: None -> 0.0" + assert _safe_float("abc") == 0.0, "_safe_float: invalid string -> 0.0" assert _safe_float(None, default=99.0) == 99.0, "_safe_float: None with custom default" # _safe_int assert _safe_int(42) == 42, "_safe_int: int passthrough" assert _safe_int("7") == 7, "_safe_int: string to int" - assert _safe_int(None) == 0, "_safe_int: None → 0" - assert _safe_int("bad") == 0, "_safe_int: invalid → 0" + assert _safe_int(None) == 0, "_safe_int: None -> 0" + assert _safe_int("bad") == 0, "_safe_int: invalid -> 0" assert _safe_int(None, default=5) == 5, "_safe_int: None with custom default" return failed @@ -250,7 +250,7 @@ def test_sigenergy_system_slug(my_predbat): failed = False api = MockSigenergyAPI() - # Long ID → last 12 chars + # Long ID -> last 12 chars slug = api._system_slug("ABCDEFGHIJKLMNOPQRSTUVWXYZ") assert len(slug) <= 12, "Slug max 12 chars: {}".format(slug) @@ -571,10 +571,10 @@ def test_sigenergy_apply_service_to_toggle(my_predbat): failed = False api = MockSigenergyAPI() - assert api._apply_service_to_toggle(False, "turn_on") is True, "turn_on → True" - assert api._apply_service_to_toggle(True, "turn_off") is False, "turn_off → False" - assert api._apply_service_to_toggle(False, "toggle") is True, "toggle False → True" - assert api._apply_service_to_toggle(True, "toggle") is False, "toggle True → False" + assert api._apply_service_to_toggle(False, "turn_on") is True, "turn_on -> True" + assert api._apply_service_to_toggle(True, "turn_off") is False, "turn_off -> False" + assert api._apply_service_to_toggle(False, "toggle") is True, "toggle False -> True" + assert api._apply_service_to_toggle(True, "toggle") is False, "toggle True -> False" assert api._apply_service_to_toggle(True, "unknown") is True, "unknown keeps current" return failed @@ -1773,9 +1773,9 @@ def test_sigenergy_fetch_inverter_realtime(my_predbat): "deviceType": "Inverter", "realTimeInfo": { "batSoc": 72.0, - "batPower": 3.0, # discharging → batteryPower should be -3.0 + "batPower": 3.0, # discharging -> batteryPower should be -3.0 "pvPower": 5.0, - "activePower": 1.5, # export → gridPower = 1.5 + "activePower": 1.5, # export -> gridPower = 1.5 "pvEnergyDaily": 12.5, }, }, @@ -1870,22 +1870,22 @@ def test_sigenergy_get_inverter_serial(my_predbat): failed = False api = MockSigenergyAPI() - # No devices → None + # No devices -> None api.devices["SYS1"] = [] assert api._get_inverter_serial("SYS1") is None, "Empty device list returns None" - # Only battery → None + # Only battery -> None api.devices["SYS1"] = [{"deviceType": "Battery", "serialNumber": "BAT001"}] assert api._get_inverter_serial("SYS1") is None, "Battery-only list returns None" - # Inverter type → found + # Inverter type -> found api.devices["SYS1"] = [ {"deviceType": "Battery", "serialNumber": "BAT001"}, {"deviceType": "Inverter", "serialNumber": "INV001"}, ] assert api._get_inverter_serial("SYS1") == "INV001", "Inverter serial returned" - # AIO type → found + # AIO type -> found api.devices["SYS2"] = [{"deviceType": "AIO", "serialNumber": "AIO001"}] assert api._get_inverter_serial("SYS2") == "AIO001", "AIO serial returned" @@ -1992,7 +1992,7 @@ def _make_api_with_system(system_id="SIG001"): def test_sigenergy_manage_vpp_registration_switch_to_msc(my_predbat): - """Readonly=True + VPP active → set_operating_mode(MSC) called, returns False.""" + """Readonly=True + VPP active -> set_operating_mode(MSC) called, returns False.""" from sigenergy import SIGENERGY_MODE_MSC failed = False sid = "SIG001" @@ -2017,7 +2017,7 @@ async def mock_set_mode(system_id, mode_int): def test_sigenergy_manage_vpp_registration_switch_to_vpp(my_predbat): - """Readonly=False + not VPP → set_operating_mode(VPP) called, returns False (activating async).""" + """Readonly=False + not VPP -> set_operating_mode(VPP) called, returns False (activating async).""" failed = False sid = "SIG001" api = _make_api_with_system(sid) @@ -2041,7 +2041,7 @@ async def mock_set_mode(system_id, mode_int): def test_sigenergy_onboard_systems_pending_per_item(my_predbat): - """onboard_systems: real API per-item response result=False codeList=[1116] → returns None and logs warning.""" + """onboard_systems: real API per-item response result=False codeList=[1116] -> returns None and logs warning.""" failed = False sid = "SIG001" api = _make_api_with_system(sid) @@ -2062,7 +2062,7 @@ async def mock_request(method, path, json_data=None, params=None, retries=3): def test_sigenergy_onboard_systems_other_vpp(my_predbat): - """onboard_systems: per-item codeList=[1103] (other VPP) → returns False and logs warning.""" + """onboard_systems: per-item codeList=[1103] (other VPP) -> returns False and logs warning.""" failed = False sid = "SIG001" api = _make_api_with_system(sid) @@ -2082,7 +2082,7 @@ async def mock_request(method, path, json_data=None, params=None, retries=3): def test_sigenergy_manage_vpp_registration_ready(my_predbat): - """Readonly=False + VPP active → no onboard/offboard, returns True.""" + """Readonly=False + VPP active -> no onboard/offboard, returns True.""" failed = False sid = "SIG001" api = _make_api_with_system(sid) @@ -2109,7 +2109,7 @@ async def mock_offboard(system_ids): def test_sigenergy_manage_vpp_registration_readonly_no_vpp(my_predbat): - """Readonly=True + VPP not active → nothing to do, returns False.""" + """Readonly=True + VPP not active -> nothing to do, returns False.""" failed = False sid = "SIG001" api = _make_api_with_system(sid) @@ -2244,7 +2244,7 @@ def test_sigenergy_update_control_time_validation(my_predbat): def test_sigenergy_offboard_toggle_in_vpp(my_predbat): - """offboard=True → return False immediately regardless of VPP state (no mode switch).""" + """offboard=True -> return False immediately regardless of VPP state (no mode switch).""" failed = False sid = "SIG001" api = _make_api_with_system(sid) @@ -2266,7 +2266,7 @@ async def mock_set_mode(system_id, mode_int): def test_sigenergy_offboard_toggle_not_in_vpp(my_predbat): - """offboard=True + not in VPP → return False, no mode switch.""" + """offboard=True + not in VPP -> return False, no mode switch.""" failed = False sid = "SIG001" api = _make_api_with_system(sid) @@ -2330,14 +2330,14 @@ def test_sigenergy_onboard_status(my_predbat): api = MockSigenergyAPI() assert api.onboard_status == {}, "onboard_status starts empty" - # Pending approval (1116) → pending_approval, returns None + # Pending approval (1116) -> pending_approval, returns None api._request = AsyncMock(return_value=None) api._last_api_code = SIGENERGY_CODE_SYSTEM_PENDING_REVIEW result = run_async(api.onboard_systems(["sys-1"])) assert result is None, "pending review returns None" assert api.onboard_status["sys-1"] == "pending_approval", "pending_approval status set" - # Registered to another VPP (1103) → in_other_vpp, returns False + # Registered to another VPP (1103) -> in_other_vpp, returns False api2 = MockSigenergyAPI() api2._request = AsyncMock(return_value=None) api2._last_api_code = SIGENERGY_CODE_IN_OTHER_VPP @@ -2420,7 +2420,7 @@ def _make_api(mode, offboard_on=False): api.apply_controls = AsyncMock() return api - # System in VPP mode → active + # System in VPP mode -> active api_active = _make_api(SIGENERGY_MODE_VPP) ok = run_async(api_active.run(seconds=300, first=False)) assert ok is True, "run() returns True on success" @@ -2429,13 +2429,13 @@ def _make_api(mode, offboard_on=False): assert api_active.dashboard_items[sensor_key]["state"] == "active", "active sensor published" assert api_active.dashboard_items[sensor_key]["attributes"]["in_vpp"] is True - # System in MSC mode, not offboarded → pending_approval + # System in MSC mode, not offboarded -> pending_approval api_pending = _make_api(SIGENERGY_MODE_MSC) run_async(api_pending.run(seconds=300, first=False)) assert api_pending.onboard_status[sid] == "pending_approval", "pending_approval derived from MSC mode" assert api_pending.dashboard_items[sensor_key]["state"] == "pending_approval" - # Offboard toggle on → offboarded regardless of mode + # Offboard toggle on -> offboarded regardless of mode api_offboard = _make_api(SIGENERGY_MODE_VPP, offboard_on=True) run_async(api_offboard.run(seconds=300, first=False)) assert api_offboard.onboard_status[sid] == "offboarded", "offboarded when toggle is on" diff --git a/apps/predbat/tests/test_solax.py b/apps/predbat/tests/test_solax.py index eb38f2fa7..a09b8594f 100644 --- a/apps/predbat/tests/test_solax.py +++ b/apps/predbat/tests/test_solax.py @@ -259,7 +259,7 @@ async def test_apply_controls(solax_api, test_plant_id): print(f"**** ERROR: Second call not self_consume_mode, got {second_call_endpoint} ****") failed = True else: - print(f"✓ ECO mode applied correctly at 12:00") + print(f"PASS: ECO mode applied correctly at 12:00") # Test 2: Charge mode (inside charge window) at 03:00 print("\n--- Test 2: Charge mode (03:00) ---") @@ -302,7 +302,7 @@ async def test_apply_controls(solax_api, test_plant_id): print(f"**** ERROR: Wrong charge power, expected 5000 got {power} ****") failed = True else: - print(f"✓ Charge mode applied correctly at 03:00 (power=5000W, target_soc=95%)") + print(f"PASS: Charge mode applied correctly at 03:00 (power=5000W, target_soc=95%)") # Test 3: Export mode (inside export window) at 18:00 print("\n--- Test 3: Export mode (18:00) ---") @@ -345,7 +345,7 @@ async def test_apply_controls(solax_api, test_plant_id): print(f"**** ERROR: Wrong export power, expected -4500 got {power} ****") failed = True else: - print(f"✓ Export mode applied correctly at 18:00 (power=-4500W, target_soc=15%)") + print(f"PASS: Export mode applied correctly at 18:00 (power=-4500W, target_soc=15%)") # Test 4: Hash prevents re-application (same charge mode at 03:00) print("\n--- Test 4: Hash caching (repeat charge at 03:00) ---") @@ -369,7 +369,7 @@ async def test_apply_controls(solax_api, test_plant_id): print(f"**** ERROR: send_command_and_wait called {mock_send.call_count} times when hash should have prevented it ****") failed = True else: - print(f"✓ Hash correctly prevented re-application of same mode") + print(f"PASS: Hash correctly prevented re-application of same mode") # Test 5: Hash expires after 15 minutes print("\n--- Test 5: Hash expiry (16 minutes later) ---") @@ -394,7 +394,7 @@ async def test_apply_controls(solax_api, test_plant_id): print(f"**** ERROR: Expected 2 API calls when hash expired, got {mock_send.call_count} ****") failed = True else: - print(f"✓ Hash correctly expired after 15 minutes, mode re-applied") + print(f"PASS: Hash correctly expired after 15 minutes, mode re-applied") # Test 6: Charge disabled - should use eco mode print("\n--- Test 6: Charge disabled (03:00) ---") @@ -428,7 +428,7 @@ async def test_apply_controls(solax_api, test_plant_id): print(f"**** ERROR: Second call not self_consume_mode, got {second_call_endpoint} ****") failed = True else: - print(f"✓ ECO mode correctly applied when charge disabled at 03:00") + print(f"PASS: ECO mode correctly applied when charge disabled at 03:00") # Test 7: Freeze charge mode (inside charge window, target_soc == current_soc) print("\n--- Test 7: Freeze charge mode (03:00, target_soc == current_soc) ---") @@ -464,7 +464,7 @@ async def test_apply_controls(solax_api, test_plant_id): print(f"**** ERROR: Second call not self_consume_charge_only_mode, got {second_call_endpoint} ****") failed = True else: - print(f"✓ Freeze charge mode applied correctly at 03:00 (target_soc == current_soc)") + print(f"PASS: Freeze charge mode applied correctly at 03:00 (target_soc == current_soc)") # Test 8: Freeze export mode (inside export window, target_soc >= current_soc) print("\n--- Test 8: Freeze export mode (18:00, target_soc >= current_soc) ---") @@ -499,7 +499,7 @@ async def test_apply_controls(solax_api, test_plant_id): print(f"**** ERROR: Second call not exit_vpp_mode, got {second_call_endpoint} ****") failed = True else: - print(f"✓ Freeze export mode applied correctly at 18:00 (target_soc >= current_soc)") + print(f"PASS: Freeze export mode applied correctly at 18:00 (target_soc >= current_soc)") # Test 9: Midnight-spanning charge window - currently after midnight (00:30) inside 23:30-05:30 window # This was the bug: charge_start (23:30 today) > now (00:30 today) so the window was missed @@ -533,7 +533,7 @@ async def test_apply_controls(solax_api, test_plant_id): print(f"**** ERROR: Expected soc_target_control_mode (charge mode) after midnight, got: {calls_str} ****") failed = True else: - print(f"✓ Midnight-spanning charge window correctly detected at 00:30") + print(f"PASS: Midnight-spanning charge window correctly detected at 00:30") # Test 10: Midnight-spanning charge window - currently before midnight (23:45) inside 23:30-05:30 window print("\n--- Test 10: Midnight-spanning charge window at 23:45 (window 23:30-05:30) ---") @@ -559,7 +559,7 @@ async def test_apply_controls(solax_api, test_plant_id): print(f"**** ERROR: Expected soc_target_control_mode (charge mode) before midnight, got: {calls_str} ****") failed = True else: - print(f"✓ Midnight-spanning charge window correctly detected at 23:45") + print(f"PASS: Midnight-spanning charge window correctly detected at 23:45") # Test 11: After window end (06:00) should be eco mode, not charge mode print("\n--- Test 11: After midnight-spanning window end at 06:00 (window 23:30-05:30) ---") @@ -588,7 +588,7 @@ async def test_apply_controls(solax_api, test_plant_id): print(f"**** ERROR: Expected eco mode (charge_or_discharge_mode) at 06:00, got: {calls_str} ****") failed = True else: - print(f"✓ After midnight-spanning window end (06:00) correctly uses eco mode") + print(f"PASS: After midnight-spanning window end (06:00) correctly uses eco mode") return failed @@ -627,7 +627,7 @@ async def test_write_setting_from_event(my_predbat): print(f"**** ERROR: Reserve not updated. Expected {new_value}, got {solax_api.controls[test_plant_id]['reserve']} ****") failed = True else: - print(f"✓ Reserve setting updated to {new_value}") + print(f"PASS: Reserve setting updated to {new_value}") # Test 2: Invalid entity ID format invalid_entity_id = "number.invalid_format" @@ -638,7 +638,7 @@ async def test_write_setting_from_event(my_predbat): print(f"**** ERROR: Reserve changed on invalid entity ID ****") failed = True else: - print(f"✓ Invalid entity ID handled gracefully") + print(f"PASS: Invalid entity ID handled gracefully") # Test 3: Plant not in controls missing_plant_id = "9999999999999999999" @@ -650,7 +650,7 @@ async def test_write_setting_from_event(my_predbat): print(f"**** ERROR: Missing plant incorrectly added to controls ****") failed = True else: - print(f"✓ Missing plant handled correctly") + print(f"PASS: Missing plant handled correctly") # Test 4: Invalid number value entity_id = f"number.{solax_api.prefix}_solax_{test_plant_id}_setting_reserve" @@ -661,7 +661,7 @@ async def test_write_setting_from_event(my_predbat): print(f"**** ERROR: Reserve changed on invalid number value ****") failed = True else: - print(f"✓ Invalid number value handled gracefully") + print(f"PASS: Invalid number value handled gracefully") # Test 5: Update charge start_time (battery schedule) entity_id = f"select.{solax_api.prefix}_solax_{test_plant_id}_battery_schedule_charge_start_time" @@ -672,7 +672,7 @@ async def test_write_setting_from_event(my_predbat): print(f"**** ERROR: Charge start_time not updated. Expected {new_time}, got {solax_api.controls[test_plant_id]['charge']['start_time']} ****") failed = True else: - print(f"✓ Charge start_time updated to {new_time}") + print(f"PASS: Charge start_time updated to {new_time}") # Test 6: Update charge end_time entity_id = f"select.{solax_api.prefix}_solax_{test_plant_id}_battery_schedule_charge_end_time" @@ -683,7 +683,7 @@ async def test_write_setting_from_event(my_predbat): print(f"**** ERROR: Charge end_time not updated. Expected {new_time}, got {solax_api.controls[test_plant_id]['charge']['end_time']} ****") failed = True else: - print(f"✓ Charge end_time updated to {new_time}") + print(f"PASS: Charge end_time updated to {new_time}") # Test 7: Update charge enable (switch - turn_on service) entity_id = f"switch.{solax_api.prefix}_solax_{test_plant_id}_battery_schedule_charge_enable" @@ -693,7 +693,7 @@ async def test_write_setting_from_event(my_predbat): print(f"**** ERROR: Charge enable not set to True ****") failed = True else: - print(f"✓ Charge enable turned on") + print(f"PASS: Charge enable turned on") # Test 8: Update charge enable (switch - turn_off service) await solax_api.switch_event(entity_id, "turn_off") @@ -702,7 +702,7 @@ async def test_write_setting_from_event(my_predbat): print(f"**** ERROR: Charge enable not set to False ****") failed = True else: - print(f"✓ Charge enable turned off") + print(f"PASS: Charge enable turned off") # Test 9: Update export start_time entity_id = f"select.{solax_api.prefix}_solax_{test_plant_id}_battery_schedule_export_start_time" @@ -713,7 +713,7 @@ async def test_write_setting_from_event(my_predbat): print(f"**** ERROR: Export start_time not updated. Expected {new_time}, got {solax_api.controls[test_plant_id]['export']['start_time']} ****") failed = True else: - print(f"✓ Export start_time updated to {new_time}") + print(f"PASS: Export start_time updated to {new_time}") # Test 10: Update charge target_soc entity_id = f"number.{solax_api.prefix}_solax_{test_plant_id}_battery_schedule_charge_target_soc" @@ -724,7 +724,7 @@ async def test_write_setting_from_event(my_predbat): print(f"**** ERROR: Charge target_soc not updated. Expected {new_soc}, got {solax_api.controls[test_plant_id]['charge']['target_soc']} ****") failed = True else: - print(f"✓ Charge target_soc updated to {new_soc}") + print(f"PASS: Charge target_soc updated to {new_soc}") # Test 11: Update charge rate entity_id = f"number.{solax_api.prefix}_solax_{test_plant_id}_battery_schedule_charge_rate" @@ -735,7 +735,7 @@ async def test_write_setting_from_event(my_predbat): print(f"**** ERROR: Charge rate not updated. Expected {new_rate}, got {solax_api.controls[test_plant_id]['charge']['rate']} ****") failed = True else: - print(f"✓ Charge rate updated to {new_rate}") + print(f"PASS: Charge rate updated to {new_rate}") # Test 12: Invalid time format (HH:MM instead of HH:MM:SS) - should be auto-fixed entity_id = f"select.{solax_api.prefix}_solax_{test_plant_id}_battery_schedule_charge_start_time" @@ -747,7 +747,7 @@ async def test_write_setting_from_event(my_predbat): print(f"**** ERROR: Time format conversion failed. Expected {expected_time}, got {solax_api.controls[test_plant_id]['charge']['start_time']} ****") failed = True else: - print(f"✓ Time format auto-converted from {new_time_short} to {expected_time}") + print(f"PASS: Time format auto-converted from {new_time_short} to {expected_time}") # Test 13: Update export target_soc entity_id = f"number.{solax_api.prefix}_solax_{test_plant_id}_battery_schedule_export_target_soc" @@ -758,7 +758,7 @@ async def test_write_setting_from_event(my_predbat): print(f"**** ERROR: Export target_soc not updated. Expected {new_export_soc}, got {solax_api.controls[test_plant_id]['export']['target_soc']} ****") failed = True else: - print(f"✓ Export target_soc updated to {new_export_soc}") + print(f"PASS: Export target_soc updated to {new_export_soc}") # Test 14: Update export rate entity_id = f"number.{solax_api.prefix}_solax_{test_plant_id}_battery_schedule_export_rate" @@ -769,7 +769,7 @@ async def test_write_setting_from_event(my_predbat): print(f"**** ERROR: Export rate not updated. Expected {new_export_rate}, got {solax_api.controls[test_plant_id]['export']['rate']} ****") failed = True else: - print(f"✓ Export rate updated to {new_export_rate}") + print(f"PASS: Export rate updated to {new_export_rate}") # Test 15: Update export end_time entity_id = f"select.{solax_api.prefix}_solax_{test_plant_id}_battery_schedule_export_end_time" @@ -780,7 +780,7 @@ async def test_write_setting_from_event(my_predbat): print(f"**** ERROR: Export end_time not updated. Expected {new_time}, got {solax_api.controls[test_plant_id]['export']['end_time']} ****") failed = True else: - print(f"✓ Export end_time updated to {new_time}") + print(f"PASS: Export end_time updated to {new_time}") # Test 16: Update export enable (switch - turn_on service) entity_id = f"switch.{solax_api.prefix}_solax_{test_plant_id}_battery_schedule_export_enable" @@ -790,7 +790,7 @@ async def test_write_setting_from_event(my_predbat): print(f"**** ERROR: Export enable not set to True ****") failed = True else: - print(f"✓ Export enable turned on") + print(f"PASS: Export enable turned on") # Test 17: Update export enable (switch - toggle service) await solax_api.switch_event(entity_id, "toggle") @@ -799,7 +799,7 @@ async def test_write_setting_from_event(my_predbat): print(f"**** ERROR: Export enable not toggled to False ****") failed = True else: - print(f"✓ Export enable toggled off") + print(f"PASS: Export enable toggled off") return failed @@ -844,7 +844,7 @@ async def test_fetch_controls(solax_api, test_plant_id): print(f"**** ERROR: Reserve not fetched correctly. Expected 25, got {solax_api.controls[test_plant_id].get('reserve')} ****") failed = True else: - print("✓ Reserve fetched correctly (25)") + print("PASS: Reserve fetched correctly (25)") # Verify charge schedule charge_controls = solax_api.controls[test_plant_id]["charge"] @@ -852,31 +852,31 @@ async def test_fetch_controls(solax_api, test_plant_id): print(f"**** ERROR: Charge start_time not fetched correctly. Expected 02:30:00, got {charge_controls.get('start_time')} ****") failed = True else: - print("✓ Charge start_time fetched correctly (02:30:00)") + print("PASS: Charge start_time fetched correctly (02:30:00)") if charge_controls.get("end_time") != "06:00:00": print(f"**** ERROR: Charge end_time not fetched correctly. Expected 06:00:00, got {charge_controls.get('end_time')} ****") failed = True else: - print("✓ Charge end_time fetched correctly (06:00:00)") + print("PASS: Charge end_time fetched correctly (06:00:00)") if charge_controls.get("enable") != True: print(f"**** ERROR: Charge enable not fetched correctly. Expected True, got {charge_controls.get('enable')} ****") failed = True else: - print("✓ Charge enable fetched correctly (True)") + print("PASS: Charge enable fetched correctly (True)") if charge_controls.get("target_soc") != 90: print(f"**** ERROR: Charge target_soc not fetched correctly. Expected 90, got {charge_controls.get('target_soc')} ****") failed = True else: - print("✓ Charge target_soc fetched correctly (90)") + print("PASS: Charge target_soc fetched correctly (90)") if charge_controls.get("rate") != 5000: print(f"**** ERROR: Charge rate not fetched correctly. Expected 5000, got {charge_controls.get('rate')} ****") failed = True else: - print("✓ Charge rate fetched correctly (5000)") + print("PASS: Charge rate fetched correctly (5000)") # Verify export schedule export_controls = solax_api.controls[test_plant_id]["export"] @@ -884,31 +884,31 @@ async def test_fetch_controls(solax_api, test_plant_id): print(f"**** ERROR: Export start_time not fetched correctly. Expected 16:30:00, got {export_controls.get('start_time')} ****") failed = True else: - print("✓ Export start_time fetched correctly (16:30:00)") + print("PASS: Export start_time fetched correctly (16:30:00)") if export_controls.get("end_time") != "19:00:00": print(f"**** ERROR: Export end_time not fetched correctly. Expected 19:00:00, got {export_controls.get('end_time')} ****") failed = True else: - print("✓ Export end_time fetched correctly (19:00:00)") + print("PASS: Export end_time fetched correctly (19:00:00)") if export_controls.get("enable") != False: print(f"**** ERROR: Export enable not fetched correctly. Expected False, got {export_controls.get('enable')} ****") failed = True else: - print("✓ Export enable fetched correctly (False)") + print("PASS: Export enable fetched correctly (False)") if export_controls.get("target_soc") != 20: print(f"**** ERROR: Export target_soc not fetched correctly. Expected 20, got {export_controls.get('target_soc')} ****") failed = True else: - print("✓ Export target_soc fetched correctly (20)") + print("PASS: Export target_soc fetched correctly (20)") if export_controls.get("rate") != 4500: print(f"**** ERROR: Export rate not fetched correctly. Expected 4500, got {export_controls.get('rate')} ****") failed = True else: - print("✓ Export rate fetched correctly (4500)") + print("PASS: Export rate fetched correctly (4500)") return failed @@ -1000,7 +1000,7 @@ async def test_publish_plant_info(solax_api, test_plant_id, test_plant_name): print(f"**** ERROR: Battery SOC unit incorrect ****") failed = True else: - print(f"✓ Battery SOC sensor published correctly ({expected_soc} kWh)") + print(f"PASS: Battery SOC sensor published correctly ({expected_soc} kWh)") # Test 2: Battery capacity sensor entity_id = f"sensor.{prefix}_solax_{test_plant_id}_battery_capacity" @@ -1013,7 +1013,7 @@ async def test_publish_plant_info(solax_api, test_plant_id, test_plant_name): print(f"**** ERROR: Battery capacity incorrect. Expected 15.0, got {item['state']} ****") failed = True else: - print(f"✓ Battery capacity sensor published correctly (15.0 kWh)") + print(f"PASS: Battery capacity sensor published correctly (15.0 kWh)") # Test 3: Battery temperature sensor entity_id = f"sensor.{prefix}_solax_{test_plant_id}_battery_temperature" @@ -1029,7 +1029,7 @@ async def test_publish_plant_info(solax_api, test_plant_id, test_plant_name): print(f"**** ERROR: Battery temperature unit incorrect ****") failed = True else: - print(f"✓ Battery temperature sensor published correctly (18.5°C)") + print(f"PASS: Battery temperature sensor published correctly (18.5°C)") # Test 4: Battery max power sensor entity_id = f"sensor.{prefix}_solax_{test_plant_id}_battery_max_power" @@ -1046,7 +1046,7 @@ async def test_publish_plant_info(solax_api, test_plant_id, test_plant_name): print(f"**** ERROR: Battery max power unit incorrect ****") failed = True else: - print(f"✓ Battery max power sensor published correctly ({expected_power}W)") + print(f"PASS: Battery max power sensor published correctly ({expected_power}W)") # Test 5: Inverter max power sensor entity_id = f"sensor.{prefix}_solax_{test_plant_id}_inverter_max_power" @@ -1063,7 +1063,7 @@ async def test_publish_plant_info(solax_api, test_plant_id, test_plant_name): print(f"**** ERROR: Inverter max power unit incorrect ****") failed = True else: - print(f"✓ Inverter max power sensor published correctly ({expected_power}W)") + print(f"PASS: Inverter max power sensor published correctly ({expected_power}W)") # Test 6: PV capacity sensor entity_id = f"sensor.{prefix}_solax_{test_plant_id}_pv_capacity" @@ -1079,7 +1079,7 @@ async def test_publish_plant_info(solax_api, test_plant_id, test_plant_name): print(f"**** ERROR: PV capacity unit incorrect ****") failed = True else: - print(f"✓ PV capacity sensor published correctly (8.5 kWp)") + print(f"PASS: PV capacity sensor published correctly (8.5 kWp)") # Test 7: Total yield sensor entity_id = f"sensor.{prefix}_solax_{test_plant_id}_total_yield" @@ -1095,7 +1095,7 @@ async def test_publish_plant_info(solax_api, test_plant_id, test_plant_name): print(f"**** ERROR: Total yield unit incorrect ****") failed = True else: - print(f"✓ Total yield sensor published correctly (3250.5 kWh)") + print(f"PASS: Total yield sensor published correctly (3250.5 kWh)") # Test 8: Total charged sensor entity_id = f"sensor.{prefix}_solax_{test_plant_id}_total_charged" @@ -1108,7 +1108,7 @@ async def test_publish_plant_info(solax_api, test_plant_id, test_plant_name): print(f"**** ERROR: Total charged incorrect. Expected 1850.2, got {item['state']} ****") failed = True else: - print(f"✓ Total charged sensor published correctly (1850.2 kWh)") + print(f"PASS: Total charged sensor published correctly (1850.2 kWh)") # Test 9: Total discharged sensor entity_id = f"sensor.{prefix}_solax_{test_plant_id}_total_discharged" @@ -1121,7 +1121,7 @@ async def test_publish_plant_info(solax_api, test_plant_id, test_plant_name): print(f"**** ERROR: Total discharged incorrect. Expected 1720.8, got {item['state']} ****") failed = True else: - print(f"✓ Total discharged sensor published correctly (1720.8 kWh)") + print(f"PASS: Total discharged sensor published correctly (1720.8 kWh)") # Test 10: Total imported sensor entity_id = f"sensor.{prefix}_solax_{test_plant_id}_total_imported" @@ -1134,7 +1134,7 @@ async def test_publish_plant_info(solax_api, test_plant_id, test_plant_name): print(f"**** ERROR: Total imported incorrect. Expected 4200.3, got {item['state']} ****") failed = True else: - print(f"✓ Total imported sensor published correctly (4200.3 kWh)") + print(f"PASS: Total imported sensor published correctly (4200.3 kWh)") # Test 11: Total exported sensor entity_id = f"sensor.{prefix}_solax_{test_plant_id}_total_exported" @@ -1147,7 +1147,7 @@ async def test_publish_plant_info(solax_api, test_plant_id, test_plant_name): print(f"**** ERROR: Total exported incorrect. Expected 2800.7, got {item['state']} ****") failed = True else: - print(f"✓ Total exported sensor published correctly (2800.7 kWh)") + print(f"PASS: Total exported sensor published correctly (2800.7 kWh)") # Test 12: Total load sensor (calculated) entity_id = f"sensor.{prefix}_solax_{test_plant_id}_total_load" @@ -1163,7 +1163,7 @@ async def test_publish_plant_info(solax_api, test_plant_id, test_plant_name): print(f"**** ERROR: Total load incorrect. Expected {expected_load}, got {item['state']} ****") failed = True else: - print(f"✓ Total load sensor published correctly ({expected_load} kWh)") + print(f"PASS: Total load sensor published correctly ({expected_load} kWh)") # Test 13: Total earnings sensor entity_id = f"sensor.{prefix}_solax_{test_plant_id}_total_earnings" @@ -1179,7 +1179,7 @@ async def test_publish_plant_info(solax_api, test_plant_id, test_plant_name): print(f"**** ERROR: Total earnings unit incorrect ****") failed = True else: - print(f"✓ Total earnings sensor published correctly (485.50)") + print(f"PASS: Total earnings sensor published correctly (485.50)") return failed @@ -1224,7 +1224,7 @@ async def test_get_access_token_main(my_predbat): print(f"**** ERROR: error_count should be 0, got {solax_api.error_count} ****") failed = True else: - print(f"✓ Successful authentication test passed") + print(f"PASS: Successful authentication test passed") # Test 2: Invalid credentials (10402) print("Test 2: Invalid credentials (code 10402)") @@ -1252,7 +1252,7 @@ async def test_get_access_token_main(my_predbat): print(f"**** ERROR: error_count should be 1, got {solax_api.error_count} ****") failed = True else: - print(f"✓ Invalid credentials test passed") + print(f"PASS: Invalid credentials test passed") # Test 3: Other error codes print("Test 3: Other API error codes") @@ -1272,7 +1272,7 @@ async def test_get_access_token_main(my_predbat): print(f"**** ERROR: error_count should be 1, got {solax_api.error_count} ****") failed = True else: - print(f"✓ Other error codes test passed") + print(f"PASS: Other error codes test passed") # Test 4: Network timeout print("Test 4: Network timeout") @@ -1291,7 +1291,7 @@ async def test_get_access_token_main(my_predbat): print(f"**** ERROR: error_count should be 1, got {solax_api.error_count} ****") failed = True else: - print(f"✓ Network timeout test passed") + print(f"PASS: Network timeout test passed") # Test 5: HTTP errors print("Test 5: HTTP 500 error") @@ -1311,7 +1311,7 @@ async def test_get_access_token_main(my_predbat): print(f"**** ERROR: error_count should be 1, got {solax_api.error_count} ****") failed = True else: - print(f"✓ HTTP error test passed") + print(f"PASS: HTTP error test passed") # Test 6: JSON decode errors print("Test 6: JSON decode error") @@ -1331,7 +1331,7 @@ async def test_get_access_token_main(my_predbat): print(f"**** ERROR: error_count should be 1, got {solax_api.error_count} ****") failed = True else: - print(f"✓ JSON decode error test passed") + print(f"PASS: JSON decode error test passed") # Test 7: Missing access_token print("Test 7: Missing access_token in response") @@ -1351,7 +1351,7 @@ async def test_get_access_token_main(my_predbat): print(f"**** ERROR: error_count should be 1, got {solax_api.error_count} ****") failed = True else: - print(f"✓ Missing access_token test passed") + print(f"PASS: Missing access_token test passed") # Test 8: Default expires_in print("Test 8: Default expires_in fallback") @@ -1389,10 +1389,10 @@ async def test_get_access_token_main(my_predbat): print(f"**** ERROR: token_expiry time difference too large for default: {time_diff} seconds ****") failed = True else: - print(f"✓ Default expires_in test passed") + print(f"PASS: Default expires_in test passed") if not failed: - print("✓ Authentication tests passed") + print("PASS: Authentication tests passed") return failed @@ -1428,7 +1428,7 @@ async def successful_func(): print(f"**** ERROR: Expected 1 call, got {call_count} ****") failed = True else: - print(f"✓ Successful call test passed") + print(f"PASS: Successful call test passed") # Test 2: ClientError with retry and eventual success print("\n--- Test 2: ClientError with retry and eventual success ---") @@ -1450,7 +1450,7 @@ async def retry_then_succeed(): print(f"**** ERROR: Expected 2 calls (1 failure + 1 success), got {call_count} ****") failed = True else: - print(f"✓ ClientError retry test passed") + print(f"PASS: ClientError retry test passed") # Test 3: TimeoutError with retry and eventual success print("\n--- Test 3: TimeoutError with retry and eventual success ---") @@ -1472,7 +1472,7 @@ async def timeout_then_succeed(): print(f"**** ERROR: Expected 2 calls (1 timeout + 1 success), got {call_count} ****") failed = True else: - print(f"✓ TimeoutError retry test passed") + print(f"PASS: TimeoutError retry test passed") # Test 4: Max retries exceeded (SOLAX_RETRIES) print("\n--- Test 4: Max retries exceeded ---") @@ -1502,7 +1502,7 @@ async def always_fail(): print(f"**** ERROR: Expected error_count to increment by 1, got {solax_api.error_count - initial_error_count} ****") failed = True else: - print(f"✓ Max retries test passed (SOLAX_RETRIES={SOLAX_RETRIES})") + print(f"PASS: Max retries test passed (SOLAX_RETRIES={SOLAX_RETRIES})") # Test 5: Unexpected exception breaks retry loop print("\n--- Test 5: Unexpected exception breaks retry loop ---") @@ -1526,7 +1526,7 @@ async def unexpected_error(): print(f"**** ERROR: Expected error_count to increment by 1, got {solax_api.error_count - initial_error_count} ****") failed = True else: - print(f"✓ Unexpected exception test passed") + print(f"PASS: Unexpected exception test passed") # Test 6: Verify exponential backoff (retry * 0.5) # Note: We mock asyncio.sleep to speed up the test @@ -1552,10 +1552,10 @@ async def fail_twice_then_succeed(): print(f"**** ERROR: Expected 3 calls (2 failures + 1 success), got {call_count} ****") failed = True else: - print(f"✓ Multiple retry test passed (exponential backoff pattern verified)") + print(f"PASS: Multiple retry test passed (exponential backoff pattern verified)") if not failed: - print("✓ request_wrapper tests passed") + print("PASS: request_wrapper tests passed") return failed @@ -1682,7 +1682,7 @@ async def test_request_get_impl_get(my_predbat): print(f"**** ERROR: Expected success response, got {result} ****") failed = True else: - print(f"✓ Successful GET with valid token test passed") + print(f"PASS: Successful GET with valid token test passed") # Test 2: Token expired - should refresh print("\n--- Test 2: Token expired - should refresh ---") @@ -1742,7 +1742,7 @@ async def session_aexit(*args): print(f"**** ERROR: Expected success after refresh, got {result} ****") failed = True else: - print(f"✓ Token expired and refreshed test passed") + print(f"PASS: Token expired and refreshed test passed") # Test 3: HTTP 404 error print("\n--- Test 3: HTTP 404 error ---") @@ -1765,7 +1765,7 @@ async def session_aexit(*args): print(f"**** ERROR: Expected error_count to increment by 1, got {solax_api.error_count - initial_error_count} ****") failed = True else: - print(f"✓ HTTP 404 error test passed") + print(f"PASS: HTTP 404 error test passed") # Test 4: HTTP 500 error print("\n--- Test 4: HTTP 500 error ---") @@ -1788,7 +1788,7 @@ async def session_aexit(*args): print(f"**** ERROR: Expected error_count to increment by 1, got {solax_api.error_count - initial_error_count} ****") failed = True else: - print(f"✓ HTTP 500 error test passed") + print(f"PASS: HTTP 500 error test passed") # Test 5: Authentication error in response (code 10401) print("\n--- Test 5: Authentication error in response (code 10401) ---") @@ -1820,7 +1820,7 @@ async def session_aexit(*args): print(f"**** ERROR: Expected error_count to increment by 1, got {solax_api.error_count - initial_error_count} ****") failed = True else: - print(f"✓ Authentication error (10401) test passed") + print(f"PASS: Authentication error (10401) test passed") # Test 6: JSON decode error print("\n--- Test 6: JSON decode error ---") @@ -1845,7 +1845,7 @@ async def session_aexit(*args): print(f"**** ERROR: Expected error_count to increment by 1, got {solax_api.error_count - initial_error_count} ****") failed = True else: - print(f"✓ JSON decode error test passed") + print(f"PASS: JSON decode error test passed") # Test 7: No token available and refresh fails print("\n--- Test 7: No token available and refresh fails ---") @@ -1864,10 +1864,10 @@ async def session_aexit(*args): print(f"**** ERROR: Expected None when token refresh fails, got {result} ****") failed = True else: - print(f"✓ Failed token refresh test passed") + print(f"PASS: Failed token refresh test passed") if not failed: - print("✓ _request_get_impl GET tests passed") + print("PASS: _request_get_impl GET tests passed") return failed @@ -1903,7 +1903,7 @@ async def test_request_get_impl_post(my_predbat): print(f"**** ERROR: Expected success response, got {result} ****") failed = True else: - print(f"✓ Successful POST with JSON body test passed") + print(f"PASS: Successful POST with JSON body test passed") # Test 2: POST with Content-Type header verification print("\n--- Test 2: POST with Content-Type header ---") @@ -1925,7 +1925,7 @@ async def test_request_get_impl_post(my_predbat): print(f"**** ERROR: Expected success response, got {result} ****") failed = True else: - print(f"✓ POST with Content-Type header test passed") + print(f"PASS: POST with Content-Type header test passed") # Test 3: POST HTTP 400 error (bad request) print("\n--- Test 3: POST HTTP 400 error ---") @@ -1949,7 +1949,7 @@ async def test_request_get_impl_post(my_predbat): print(f"**** ERROR: Expected error_count to increment by 1, got {solax_api.error_count - initial_error_count} ****") failed = True else: - print(f"✓ POST HTTP 400 error test passed") + print(f"PASS: POST HTTP 400 error test passed") # Test 4: POST with authentication error in response print("\n--- Test 4: POST with authentication error (code 10400) ---") @@ -1982,7 +1982,7 @@ async def test_request_get_impl_post(my_predbat): print(f"**** ERROR: Expected error_count to increment by 1 ****") failed = True else: - print(f"✓ POST authentication error test passed") + print(f"PASS: POST authentication error test passed") # Test 5: POST with JSON decode error print("\n--- Test 5: POST with JSON decode error ---") @@ -2008,7 +2008,7 @@ async def test_request_get_impl_post(my_predbat): print(f"**** ERROR: Expected error_count to increment by 1 ****") failed = True else: - print(f"✓ POST JSON decode error test passed") + print(f"PASS: POST JSON decode error test passed") # Test 6: POST with empty body print("\n--- Test 6: POST with empty body ---") @@ -2027,10 +2027,10 @@ async def test_request_get_impl_post(my_predbat): print(f"**** ERROR: Expected success response, got {result} ****") failed = True else: - print(f"✓ POST with empty body test passed") + print(f"PASS: POST with empty body test passed") if not failed: - print("✓ _request_get_impl POST tests passed") + print("PASS: _request_get_impl POST tests passed") return failed @@ -2082,7 +2082,7 @@ async def test_fetch_paginated_data(my_predbat): print(f"**** ERROR: Record data mismatch ****") failed = True else: - print(f"✓ Single page with records test passed") + print(f"PASS: Single page with records test passed") # Test 2: Multiple pages (3 pages) print("\n--- Test 2: Multiple pages (3 pages) ---") @@ -2175,7 +2175,7 @@ async def session_aexit(*args): print(f"**** ERROR: Records not collected correctly across pages ****") failed = True else: - print(f"✓ Multiple pages (3 pages) test passed") + print(f"PASS: Multiple pages (3 pages) test passed") # Test 3: Empty result (no records) print("\n--- Test 3: Empty result (no records) ---") @@ -2208,7 +2208,7 @@ async def session_aexit(*args): print(f"**** ERROR: Expected 0 records, got {len(result)} ****") failed = True else: - print(f"✓ Empty result test passed") + print(f"PASS: Empty result test passed") # Test 4: API error on first page print("\n--- Test 4: API error on first page ---") @@ -2231,7 +2231,7 @@ async def session_aexit(*args): print(f"**** ERROR: Expected error_count to increment by 1, got {solax_api.error_count - initial_error_count} ****") failed = True else: - print(f"✓ API error on first page test passed") + print(f"PASS: API error on first page test passed") # Test 5: Network failure on second page print("\n--- Test 5: Network failure on second page ---") @@ -2294,7 +2294,7 @@ async def session_aexit(*args): print(f"**** ERROR: Expected None when second page fails, got {result} ****") failed = True else: - print(f"✓ Network failure on second page test passed") + print(f"PASS: Network failure on second page test passed") # Test 6: Missing result field in response print("\n--- Test 6: Missing result field in response ---") @@ -2322,10 +2322,10 @@ async def session_aexit(*args): print(f"**** ERROR: Expected 0 records for missing result field, got {len(result)} ****") failed = True else: - print(f"✓ Missing result field test passed") + print(f"PASS: Missing result field test passed") if not failed: - print("✓ fetch_paginated_data tests passed") + print("PASS: fetch_paginated_data tests passed") return failed @@ -2373,7 +2373,7 @@ async def test_fetch_single_result(my_predbat): print(f"**** ERROR: Expected requestId 'req-123-456', got {request_id} ****") failed = True else: - print(f"✓ Successful GET request test passed") + print(f"PASS: Successful GET request test passed") # Test 2: Successful POST request with result print("\n--- Test 2: Successful POST request with result ---") @@ -2406,7 +2406,7 @@ async def test_fetch_single_result(my_predbat): print(f"**** ERROR: Expected requestId 'req-789', got {request_id} ****") failed = True else: - print(f"✓ Successful POST request test passed") + print(f"PASS: Successful POST request test passed") # Test 3: Empty result field print("\n--- Test 3: Empty result field ---") @@ -2437,7 +2437,7 @@ async def test_fetch_single_result(my_predbat): print(f"**** ERROR: Expected requestId 'req-empty', got {request_id} ****") failed = True else: - print(f"✓ Empty result field test passed") + print(f"PASS: Empty result field test passed") # Test 4: API error code print("\n--- Test 4: API error code (10001) ---") @@ -2463,7 +2463,7 @@ async def test_fetch_single_result(my_predbat): print(f"**** ERROR: Expected error_count to increment by 1, got {solax_api.error_count - initial_error_count} ****") failed = True else: - print(f"✓ API error code test passed") + print(f"PASS: API error code test passed") # Test 5: Network failure (response is None) print("\n--- Test 5: Network failure (response is None) ---") @@ -2485,7 +2485,7 @@ async def test_fetch_single_result(my_predbat): print(f"**** ERROR: Expected None requestId for network failure, got {request_id} ****") failed = True else: - print(f"✓ Network failure test passed") + print(f"PASS: Network failure test passed") # Test 6: Missing requestId field print("\n--- Test 6: Missing requestId field ---") @@ -2517,10 +2517,10 @@ async def test_fetch_single_result(my_predbat): print(f"**** ERROR: Expected empty string for missing requestId, got {request_id} ****") failed = True else: - print(f"✓ Missing requestId field test passed") + print(f"PASS: Missing requestId field test passed") if not failed: - print("✓ fetch_single_result tests passed") + print("PASS: fetch_single_result tests passed") return failed @@ -2576,7 +2576,7 @@ async def test_query_plant_info(my_predbat): print(f"**** ERROR: plant_info not updated correctly ****") failed = True else: - print(f"✓ Successful query with multiple plants test passed") + print(f"PASS: Successful query with multiple plants test passed") # Test 2: Query with plant ID filter print("\n--- Test 2: Query with plant ID filter ---") @@ -2613,7 +2613,7 @@ async def test_query_plant_info(my_predbat): print(f"**** ERROR: Wrong plant returned ****") failed = True else: - print(f"✓ Query with plant ID filter test passed") + print(f"PASS: Query with plant ID filter test passed") # Test 3: Empty result (no plants found) print("\n--- Test 3: Empty result (no plants found) ---") @@ -2650,7 +2650,7 @@ async def test_query_plant_info(my_predbat): print(f"**** ERROR: plant_info should be empty list ****") failed = True else: - print(f"✓ Empty result test passed") + print(f"PASS: Empty result test passed") # Test 4: API error during query print("\n--- Test 4: API error during query (code 10001) ---") @@ -2674,7 +2674,7 @@ async def test_query_plant_info(my_predbat): print(f"**** ERROR: plant_info should not be updated on error ****") failed = True else: - print(f"✓ API error during query test passed") + print(f"PASS: API error during query test passed") # Test 5: Multi-page plant results print("\n--- Test 5: Multi-page plant results ---") @@ -2755,10 +2755,10 @@ async def session_aexit(*args): print(f"**** ERROR: Plants not collected correctly across pages ****") failed = True else: - print(f"✓ Multi-page plant results test passed") + print(f"PASS: Multi-page plant results test passed") if not failed: - print("✓ query_plant_info tests passed") + print("PASS: query_plant_info tests passed") return failed @@ -2829,7 +2829,7 @@ async def test_query_device_info(my_predbat): print(f"**** ERROR: deviceType not set correctly ****") failed = True else: - print(f"✓ Inverter devices query test passed") + print(f"PASS: Inverter devices query test passed") # Test 2: Query battery devices (device_type=2) print("\n--- Test 2: Query battery devices (device_type=2) ---") @@ -2876,7 +2876,7 @@ async def test_query_device_info(my_predbat): print(f"**** ERROR: Battery deviceType not set correctly ****") failed = True else: - print(f"✓ Battery devices query test passed") + print(f"PASS: Battery devices query test passed") # Test 3: Query with device serial number filter print("\n--- Test 3: Query with device serial number filter ---") @@ -2917,7 +2917,7 @@ async def test_query_device_info(my_predbat): print(f"**** ERROR: Wrong device returned ****") failed = True else: - print(f"✓ Device serial number filter test passed") + print(f"PASS: Device serial number filter test passed") # Test 4: Empty result (no devices found) print("\n--- Test 4: Empty result (no devices found) ---") @@ -2950,7 +2950,7 @@ async def test_query_device_info(my_predbat): print(f"**** ERROR: Expected 0 devices, got {len(result)} ****") failed = True else: - print(f"✓ Empty result test passed") + print(f"PASS: Empty result test passed") # Test 5: API error during query print("\n--- Test 5: API error during query (code 10001) ---") @@ -2969,7 +2969,7 @@ async def test_query_device_info(my_predbat): print(f"**** ERROR: Expected None for API error, got {result} ****") failed = True else: - print(f"✓ API error during query test passed") + print(f"PASS: API error during query test passed") # Test 6: Multiple device types in same plant print("\n--- Test 6: Multiple device types in same plant ---") @@ -3056,7 +3056,7 @@ async def session_aexit(*args): print(f"**** ERROR: Battery not in plant_batteries ****") failed = True else: - print(f"✓ Multiple device types in same plant test passed") + print(f"PASS: Multiple device types in same plant test passed") # Test 7: Device without serial number (edge case) print("\n--- Test 7: Device without serial number (edge case) ---") @@ -3095,10 +3095,10 @@ async def session_aexit(*args): print(f"**** ERROR: Device with SN should be stored ****") failed = True else: - print(f"✓ Device without serial number test passed") + print(f"PASS: Device without serial number test passed") if not failed: - print("✓ query_device_info tests passed") + print("PASS: query_device_info tests passed") return failed @@ -3153,7 +3153,7 @@ async def mock_fetch_success(path, params=None, post=False, json_data=None): print(f"**** ERROR: Stored data mismatch ****") failed = True else: - print(f"✓ Successful fetch test passed") + print(f"PASS: Successful fetch test passed") # Test 2: API error response (non-10000 code) print("Test 2: API error response") @@ -3175,7 +3175,7 @@ async def mock_fetch_error(path, params=None, post=False, json_data=None): print(f"**** ERROR: Data should not be stored on error ****") failed = True else: - print(f"✓ API error test passed") + print(f"PASS: API error test passed") # Test 3: Multiple plants with different data print("Test 3: Multiple plants with different data") @@ -3218,7 +3218,7 @@ async def mock_fetch_multiple(path, params=None, post=False, json_data=None): print(f"**** ERROR: Plant 2 data mismatch ****") failed = True else: - print(f"✓ Multiple plants test passed") + print(f"PASS: Multiple plants test passed") # Test 4: Custom business_type parameter print("Test 4: Custom business_type parameter") @@ -3243,7 +3243,7 @@ async def mock_fetch_capture_params(path, params=None, post=False, json_data=Non print(f"**** ERROR: Expected business_type 1, got {captured_params[0].get('businessType')} ****") failed = True else: - print(f"✓ Default business_type test passed") + print(f"PASS: Default business_type test passed") # Test with custom business_type captured_params.clear() @@ -3256,7 +3256,7 @@ async def mock_fetch_capture_params(path, params=None, post=False, json_data=Non print(f"**** ERROR: Expected business_type 4, got {captured_params[0].get('businessType')} ****") failed = True else: - print(f"✓ Custom business_type test passed") + print(f"PASS: Custom business_type test passed") # Test 5: Empty result (valid API response but no data) print("Test 5: Empty result dictionary") @@ -3277,7 +3277,7 @@ async def mock_fetch_empty(path, params=None, post=False, json_data=None): print(f"**** ERROR: Empty result should still be stored ****") failed = True else: - print(f"✓ Empty result test passed") + print(f"PASS: Empty result test passed") # Test 6: Overwrite existing data with new fetch print("Test 6: Overwrite existing data with new fetch") @@ -3312,10 +3312,10 @@ async def mock_fetch_overwrite(path, params=None, post=False, json_data=None): print(f"**** ERROR: Data not overwritten correctly ****") failed = True else: - print(f"✓ Overwrite existing data test passed") + print(f"PASS: Overwrite existing data test passed") if not failed: - print("✓ query_plant_realtime_data tests passed") + print("PASS: query_plant_realtime_data tests passed") return failed @@ -3375,7 +3375,7 @@ async def mock_fetch_inverter(path, params=None, post=False, json_data=None): print(f"**** ERROR: Stored data mismatch ****") failed = True else: - print(f"✓ Successful inverter fetch test passed") + print(f"PASS: Successful inverter fetch test passed") # Test 2: Successful fetch for battery device (device_type=2) print("Test 2: Successful fetch for battery device") @@ -3422,7 +3422,7 @@ async def mock_fetch_battery(path, params=None, post=False, json_data=None): print(f"**** ERROR: Stored battery data mismatch ****") failed = True else: - print(f"✓ Successful battery fetch test passed") + print(f"PASS: Successful battery fetch test passed") # Test 3: API error response (fetch_single_result returns None) print("Test 3: API error response") @@ -3443,7 +3443,7 @@ async def mock_fetch_error(path, params=None, post=False, json_data=None): print(f"**** ERROR: Data should not be stored on error ****") failed = True else: - print(f"✓ API error test passed") + print(f"PASS: API error test passed") # Test 4: Empty result list print("Test 4: Empty result list") @@ -3464,7 +3464,7 @@ async def mock_fetch_empty(path, params=None, post=False, json_data=None): print(f"**** ERROR: Data should not be stored for empty result ****") failed = True else: - print(f"✓ Empty result test passed") + print(f"PASS: Empty result test passed") # Test 5: Custom business_type parameter print("Test 5: Custom business_type parameter") @@ -3495,7 +3495,7 @@ async def mock_fetch_capture(path, params=None, post=False, json_data=None): print(f"**** ERROR: Expected snList ['TEST_SN'], got {captured_params[0].get('snList')} ****") failed = True else: - print(f"✓ Default business_type test passed") + print(f"PASS: Default business_type test passed") # Test with custom business_type captured_params.clear() @@ -3511,7 +3511,7 @@ async def mock_fetch_capture(path, params=None, post=False, json_data=None): print(f"**** ERROR: Expected device_type 2, got {captured_params[0].get('deviceType')} ****") failed = True else: - print(f"✓ Custom business_type test passed") + print(f"PASS: Custom business_type test passed") # Test 6: Multiple devices queried separately print("Test 6: Multiple devices queried separately") @@ -3544,7 +3544,7 @@ async def mock_fetch_multiple(path, params=None, post=False, json_data=None): print(f"**** ERROR: DEV_003 data mismatch ****") failed = True else: - print(f"✓ Multiple devices test passed") + print(f"PASS: Multiple devices test passed") # Test 7: Overwrite existing device data print("Test 7: Overwrite existing device data with new fetch") @@ -3573,7 +3573,7 @@ async def mock_fetch_overwrite(path, params=None, post=False, json_data=None): print(f"**** ERROR: Data not overwritten correctly ****") failed = True else: - print(f"✓ Overwrite existing device data test passed") + print(f"PASS: Overwrite existing device data test passed") # Test 8: real_sn != sn (shared Device ID / fake battery suffix) # When real_sn differs from sn, snList must use real_sn and requestSnType must be set. @@ -3606,7 +3606,7 @@ async def mock_fetch_capture_params(path, params=None, post=False, json_data=Non print(f"**** ERROR: Result stored under wrong key or data incorrect ****") failed = True else: - print(f"✓ real_sn != sn request params test passed") + print(f"PASS: real_sn != sn request params test passed") # Also verify that when real_sn == sn, requestSnType is NOT added to params print("Test 8b: real_sn == sn - requestSnType is absent") @@ -3635,10 +3635,10 @@ async def mock_fetch_no_override(path, params=None, post=False, json_data=None): print(f"**** ERROR: requestSnType should not be present when real_sn == sn, got {p.get('requestSnType')} ****") failed = True else: - print(f"✓ real_sn == sn no requestSnType test passed") + print(f"PASS: real_sn == sn no requestSnType test passed") if not failed: - print("✓ query_device_realtime_data tests passed") + print("PASS: query_device_realtime_data tests passed") return failed @@ -3685,7 +3685,7 @@ async def mock_query_device(sn, device_type, business_type=None, real_sn=None): print(f"**** ERROR: Unexpected device SNs called ****") failed = True else: - print(f"✓ Successful fetch with multiple devices test passed") + print(f"PASS: Successful fetch with multiple devices test passed") # Test 2: Empty device_info (no devices) print("Test 2: Empty device_info (no devices)") @@ -3711,7 +3711,7 @@ async def mock_query_never_called(sn, device_type, business_type=None, real_sn=N print(f"**** ERROR: query_device_realtime_data should not be called when no devices ****") failed = True else: - print(f"✓ Empty device_info test passed") + print(f"PASS: Empty device_info test passed") # Test 3: Some devices return None (error handling) print("Test 3: Some devices return None (error handling)") @@ -3740,7 +3740,7 @@ async def mock_query_with_error(sn, device_type, business_type=None, real_sn=Non print(f"**** ERROR: Result data mismatch ****") failed = True else: - print(f"✓ Error handling test passed") + print(f"PASS: Error handling test passed") # Test 4: Custom business_type parameter print("Test 4: Custom business_type parameter") @@ -3769,7 +3769,7 @@ async def mock_query_capture_business_type(sn, device_type, business_type=None, print(f"**** ERROR: Expected business_type None, got {captured_business_type[0]} ****") failed = True else: - print(f"✓ Default business_type test passed") + print(f"PASS: Default business_type test passed") # Test with custom business_type captured_business_type.clear() @@ -3779,7 +3779,7 @@ async def mock_query_capture_business_type(sn, device_type, business_type=None, print(f"**** ERROR: Expected business_type 4, got {captured_business_type[0]} ****") failed = True else: - print(f"✓ Custom business_type test passed") + print(f"PASS: Custom business_type test passed") # Test 5: Mixed device types (verify deviceType extracted correctly) print("Test 5: Mixed device types verification") @@ -3821,7 +3821,7 @@ async def mock_query_log_types(sn, device_type, business_type=None, real_sn=None print(f"**** ERROR: Meter device_type incorrect ****") failed = True else: - print(f"✓ Mixed device types verification test passed") + print(f"PASS: Mixed device types verification test passed") # Test 6: Results are aggregated correctly (extend not append) print("Test 6: Results aggregation (extend behavior)") @@ -3845,7 +3845,7 @@ async def mock_query_multi_records(sn, device_type, business_type=None, real_sn= print(f"**** ERROR: Expected 4 total records (2 devices x 2 records), got {len(result6)} ****") failed = True else: - print(f"✓ Results aggregation test passed") + print(f"PASS: Results aggregation test passed") # Test 7: Shared Device ID (battery SN has _battery suffix, real_sn strips it) print("Test 7: Shared Device ID - battery uses inverter SN with _battery suffix") @@ -3889,10 +3889,10 @@ async def mock_query_capture_real_sn(sn, device_type, business_type=None, real_s print(f"**** ERROR: Battery real_sn should be 'INV001' (suffix stripped), got '{bat_call['real_sn']}' ****") failed = True else: - print(f"✓ Shared Device ID real_sn test passed") + print(f"PASS: Shared Device ID real_sn test passed") if not failed: - print("✓ query_device_realtime_data_all tests passed") + print("PASS: query_device_realtime_data_all tests passed") return failed @@ -3951,7 +3951,7 @@ async def mock_query_statistics(plant_id, date_type, date, business_type=None): print(f"**** ERROR: Expected date format YYYY-MM, got {captured_calls[0]['date']} ****") failed = True else: - print(f"✓ Successful fetch for current month test passed") + print(f"PASS: Successful fetch for current month test passed") # Test 2: API error response (None returned) print("Test 2: API error response") @@ -3969,7 +3969,7 @@ async def mock_query_error(plant_id, date_type, date, business_type=None): print(f"**** ERROR: Expected None on API error, got {result2} ****") failed = True else: - print(f"✓ API error response test passed") + print(f"PASS: API error response test passed") # Test 3: Custom business_type parameter print("Test 3: Custom business_type parameter") @@ -3994,7 +3994,7 @@ async def mock_query_capture_business_type(plant_id, date_type, date, business_t print(f"**** ERROR: Expected business_type None, got {captured_business_type[0]} ****") failed = True else: - print(f"✓ Default business_type test passed") + print(f"PASS: Default business_type test passed") # Test with custom business_type captured_business_type.clear() @@ -4004,7 +4004,7 @@ async def mock_query_capture_business_type(plant_id, date_type, date, business_t print(f"**** ERROR: Expected business_type 4, got {captured_business_type[0]} ****") failed = True else: - print(f"✓ Custom business_type test passed") + print(f"PASS: Custom business_type test passed") # Test 4: Empty plantEnergyStatDataList (no data for month) print("Test 4: Empty plantEnergyStatDataList (no data for month)") @@ -4025,7 +4025,7 @@ async def mock_query_empty_data(plant_id, date_type, date, business_type=None): print(f"**** ERROR: Expected empty list, got {len(result4['plantEnergyStatDataList'])} records ****") failed = True else: - print(f"✓ Empty plantEnergyStatDataList test passed") + print(f"PASS: Empty plantEnergyStatDataList test passed") # Test 5: Verify date format passed to query_plant_statistics print("Test 5: Verify date format passed to query_plant_statistics") @@ -4051,10 +4051,10 @@ async def mock_query_capture_date(plant_id, date_type, date, business_type=None) print(f"**** ERROR: Expected YYYY-MM format, got {captured_date_formats[0]} ****") failed = True else: - print(f"✓ Date format verification test passed") + print(f"PASS: Date format verification test passed") if not failed: - print("✓ query_plant_statistics_daily tests passed") + print("PASS: query_plant_statistics_daily tests passed") return failed @@ -4090,7 +4090,7 @@ async def mock_query_result_immediate_success(request_id): print(f"**** ERROR: Expected True for successful execution, got {result} ****") failed = True else: - print(f"✓ Immediate success test passed") + print(f"PASS: Immediate success test passed") # Test 2: Command issuance failed (device offline) print("Test 2: Command issuance failed (device offline)") @@ -4108,7 +4108,7 @@ async def mock_fetch_offline(endpoint, post, json_data): print(f"**** ERROR: Expected False for offline device, got {result2} ****") failed = True else: - print(f"✓ Device offline test passed") + print(f"PASS: Device offline test passed") # Test 3: fetch_single_result returns None (network error) print("Test 3: fetch_single_result returns None (network error)") @@ -4126,7 +4126,7 @@ async def mock_fetch_none(endpoint, post, json_data): print(f"**** ERROR: Expected False for None result, got {result3} ****") failed = True else: - print(f"✓ Network error test passed") + print(f"PASS: Network error test passed") # Test 4: Successful after polling retries print("Test 4: Successful after polling retries") @@ -4158,7 +4158,7 @@ async def mock_query_result_delayed(request_id): print(f"**** ERROR: Expected 3 polling attempts, got {poll_count[0]} ****") failed = True else: - print(f"✓ Polling retry test passed") + print(f"PASS: Polling retry test passed") # Test 5: Execution failed after polling print("Test 5: Execution failed after polling") @@ -4179,7 +4179,7 @@ async def mock_query_result_exec_failed(request_id): print(f"**** ERROR: Expected False for execution failure, got {result5} ****") failed = True else: - print(f"✓ Execution failed test passed") + print(f"PASS: Execution failed test passed") # Test 6: Timeout after max retries print("Test 6: Timeout after max retries") @@ -4208,7 +4208,7 @@ async def mock_query_result_always_pending(request_id): print(f"**** ERROR: Expected {SOLAX_COMMAND_MAX_RETRIES} polling attempts, got {retry_count[0]} ****") failed = True else: - print(f"✓ Timeout after max retries test passed") + print(f"PASS: Timeout after max retries test passed") # Test 7: No request_id returned (edge case) print("Test 7: No request_id returned (edge case)") @@ -4226,10 +4226,10 @@ async def mock_fetch_no_request_id(endpoint, post, json_data): print(f"**** ERROR: Expected False for missing request_id, got {result7} ****") failed = True else: - print(f"✓ Missing request_id test passed") + print(f"PASS: Missing request_id test passed") if not failed: - print("✓ send_command_and_wait tests passed") + print("PASS: send_command_and_wait tests passed") return failed @@ -4279,7 +4279,7 @@ async def mock_send_command(endpoint, payload, command_name, sn_list): print(f"**** ERROR: Wrong command_name: {captured_calls[0]['command_name']} ****") failed = True else: - print(f"✓ self_consume_mode successful execution test passed") + print(f"PASS: self_consume_mode successful execution test passed") # Test 2: self_consume_mode() - command failed print("Test 2: self_consume_mode() - command failed") @@ -4297,7 +4297,7 @@ async def mock_send_command_failed(endpoint, payload, command_name, sn_list): print(f"**** ERROR: Expected False for failed command, got {result2} ****") failed = True else: - print(f"✓ self_consume_mode command failed test passed") + print(f"PASS: self_consume_mode command failed test passed") # Test 3: self_consume_mode() - custom business_type print("Test 3: self_consume_mode() - custom business_type") @@ -4313,7 +4313,7 @@ async def mock_send_command_failed(endpoint, payload, command_name, sn_list): print(f"**** ERROR: Expected business_type 4, got {captured_calls[0]['payload']['businessType']} ****") failed = True else: - print(f"✓ self_consume_mode custom business_type test passed") + print(f"PASS: self_consume_mode custom business_type test passed") # Test 4: soc_target_control_mode() - charge mode (positive power) print("Test 4: soc_target_control_mode() - charge mode (positive power)") @@ -4341,7 +4341,7 @@ async def mock_send_command_failed(endpoint, payload, command_name, sn_list): print(f"**** ERROR: Wrong command_name ****") failed = True else: - print(f"✓ soc_target_control_mode charge mode test passed") + print(f"PASS: soc_target_control_mode charge mode test passed") # Test 5: soc_target_control_mode() - discharge mode (negative power) print("Test 5: soc_target_control_mode() - discharge mode (negative power)") @@ -4363,7 +4363,7 @@ async def mock_send_command_failed(endpoint, payload, command_name, sn_list): print(f"**** ERROR: Expected power -4500, got {captured_calls[0]['payload']['chargeDischargPower']} ****") # cspell:disable-line failed = True else: - print(f"✓ soc_target_control_mode discharge mode test passed") + print(f"PASS: soc_target_control_mode discharge mode test passed") # Test 6: set_work_mode() - selfuse mode print("Test 6: set_work_mode() - selfuse mode") @@ -4397,7 +4397,7 @@ async def mock_send_command_failed(endpoint, payload, command_name, sn_list): print(f"**** ERROR: Wrong command_name ****") failed = True else: - print(f"✓ set_work_mode selfuse mode test passed") + print(f"PASS: set_work_mode selfuse mode test passed") # Test 7: set_work_mode() - backup mode print("Test 7: set_work_mode() - backup mode") @@ -4419,7 +4419,7 @@ async def mock_send_command_failed(endpoint, payload, command_name, sn_list): print(f"**** ERROR: Wrong command_name ****") failed = True else: - print(f"✓ set_work_mode backup mode test passed") + print(f"PASS: set_work_mode backup mode test passed") # Test 8: set_work_mode() - feedin mode print("Test 8: set_work_mode() - feedin mode") @@ -4441,7 +4441,7 @@ async def mock_send_command_failed(endpoint, payload, command_name, sn_list): print(f"**** ERROR: Wrong command_name ****") failed = True else: - print(f"✓ set_work_mode feedin mode test passed") + print(f"PASS: set_work_mode feedin mode test passed") # Test 9: set_work_mode() - unknown mode print("Test 9: set_work_mode() - unknown mode") @@ -4460,7 +4460,7 @@ async def mock_send_command_failed(endpoint, payload, command_name, sn_list): print(f"**** ERROR: Expected no calls for unknown mode, got {len(captured_calls)} ****") failed = True else: - print(f"✓ set_work_mode unknown mode test passed") + print(f"PASS: set_work_mode unknown mode test passed") # Test 10: Multiple devices in sn_list print("Test 10: Multiple devices in sn_list") @@ -4479,10 +4479,10 @@ async def mock_send_command_failed(endpoint, payload, command_name, sn_list): print(f"**** ERROR: Wrong sn_list in payload ****") failed = True else: - print(f"✓ Multiple devices test passed") + print(f"PASS: Multiple devices test passed") if not failed: - print("✓ control mode functions tests passed") + print("PASS: control mode functions tests passed") return failed @@ -4530,7 +4530,7 @@ async def test_publish_device_info_main(): print(f"**** ERROR: Incorrect friendly_name: {attrs.get('friendly_name')} ****") failed = True else: - print(f"✓ Inverter device info publishing test passed") + print(f"PASS: Inverter device info publishing test passed") # Test 2: Battery device info publishing print("Test 2: Battery device info publishing") @@ -4558,7 +4558,7 @@ async def test_publish_device_info_main(): print(f"**** ERROR: Expected rated_power 5000W, got {attrs.get('rated_power')} ****") failed = True else: - print(f"✓ Battery device info publishing test passed") + print(f"PASS: Battery device info publishing test passed") # Test 3: Meter device info publishing print("Test 3: Meter device info publishing") @@ -4582,7 +4582,7 @@ async def test_publish_device_info_main(): print(f"**** ERROR: Expected device_model 'Meter X', got {attrs.get('device_model')} ****") failed = True else: - print(f"✓ Meter device info publishing test passed") + print(f"PASS: Meter device info publishing test passed") # Test 4: EV Charger device info publishing print("Test 4: EV Charger device info publishing") @@ -4606,7 +4606,7 @@ async def test_publish_device_info_main(): print(f"**** ERROR: Expected rated_power 7000W, got {attrs.get('rated_power')} ****") failed = True else: - print(f"✓ EV Charger device info publishing test passed") + print(f"PASS: EV Charger device info publishing test passed") # Test 5: Unknown device type print("Test 5: Unknown device type") @@ -4627,7 +4627,7 @@ async def test_publish_device_info_main(): print(f"**** ERROR: Expected device_model 'Unknown Device', got {attrs.get('device_model')} ****") failed = True else: - print(f"✓ Unknown device type test passed") + print(f"PASS: Unknown device type test passed") # Test 6: Multiple devices print("Test 6: Multiple devices") @@ -4669,7 +4669,7 @@ async def test_publish_device_info_main(): print(f"**** ERROR: Wrong meter model: {mtr_attrs.get('device_model')} ****") failed = True else: - print(f"✓ Multiple devices test passed") + print(f"PASS: Multiple devices test passed") # Test 7: Empty device_info (no devices to publish) print("Test 7: Empty device_info") @@ -4686,7 +4686,7 @@ async def test_publish_device_info_main(): print(f"**** ERROR: Expected no sensors, got {len(api7.dashboard_items)} ****") failed = True else: - print(f"✓ Empty device_info test passed") + print(f"PASS: Empty device_info test passed") # Test 8: Missing optional fields (should use defaults) print("Test 8: Missing optional fields") @@ -4720,10 +4720,10 @@ async def test_publish_device_info_main(): print(f"**** ERROR: Expected plant_id None, got {attrs.get('plant_id')} ****") failed = True else: - print(f"✓ Missing optional fields test passed") + print(f"PASS: Missing optional fields test passed") if not failed: - print("✓ publish_device_info tests passed") + print("PASS: publish_device_info tests passed") return failed @@ -4771,7 +4771,7 @@ async def test_publish_device_realtime_data_main(): print(f"**** ERROR: Expected status_value 102, got {api.dashboard_items[sensor_id]['attributes']['status_value']} ****") failed = True else: - print(f"✓ Inverter device status sensor correct") + print(f"PASS: Inverter device status sensor correct") # Verify AC power sensor (sum of 3 phases) sensor_id = "sensor.predbat_solax_1618699116555534337_H1231231932123_ac_power" @@ -4782,7 +4782,7 @@ async def test_publish_device_realtime_data_main(): print(f"**** ERROR: Expected AC power 3000, got {api.dashboard_items[sensor_id]['state']} ****") failed = True else: - print(f"✓ Inverter AC power sensor correct") + print(f"PASS: Inverter AC power sensor correct") # Verify PV power sensor (sum from pvMap) sensor_id = "sensor.predbat_solax_1618699116555534337_H1231231932123_pv_power" @@ -4793,7 +4793,7 @@ async def test_publish_device_realtime_data_main(): print(f"**** ERROR: Expected PV power 3500, got {api.dashboard_items[sensor_id]['state']} ****") failed = True else: - print(f"✓ Inverter PV power sensor correct") + print(f"PASS: Inverter PV power sensor correct") # Verify grid power sensor sensor_id = "sensor.predbat_solax_1618699116555534337_H1231231932123_grid_power" @@ -4804,7 +4804,7 @@ async def test_publish_device_realtime_data_main(): print(f"**** ERROR: Expected grid power -2500, got {api.dashboard_items[sensor_id]['state']} ****") failed = True else: - print(f"✓ Inverter grid power sensor correct") + print(f"PASS: Inverter grid power sensor correct") # Verify total yield sensor sensor_id = "sensor.predbat_solax_1618699116555534337_H1231231932123_total_yield" @@ -4815,7 +4815,7 @@ async def test_publish_device_realtime_data_main(): print(f"**** ERROR: Expected total yield 12500.5, got {api.dashboard_items[sensor_id]['state']} ****") failed = True else: - print(f"✓ Inverter total yield sensor correct") + print(f"PASS: Inverter total yield sensor correct") # Test 2: Battery realtime data publishing print("Test 2: Battery realtime data publishing") @@ -4839,7 +4839,7 @@ async def test_publish_device_realtime_data_main(): print(f"**** ERROR: Expected status 'Work', got {api2.dashboard_items[sensor_id]['state']} ****") failed = True else: - print(f"✓ Battery device status sensor correct") + print(f"PASS: Battery device status sensor correct") # Verify battery SOC sensor sensor_id = "sensor.predbat_solax_1618699116555534337_TP123456123123_battery_soc" @@ -4853,7 +4853,7 @@ async def test_publish_device_realtime_data_main(): print(f"**** ERROR: Wrong SOC unit ****") failed = True else: - print(f"✓ Battery SOC sensor correct") + print(f"PASS: Battery SOC sensor correct") # Verify battery voltage sensor sensor_id = "sensor.predbat_solax_1618699116555534337_TP123456123123_battery_voltage" @@ -4864,7 +4864,7 @@ async def test_publish_device_realtime_data_main(): print(f"**** ERROR: Expected voltage 450.5V, got {api2.dashboard_items[sensor_id]['state']} ****") failed = True else: - print(f"✓ Battery voltage sensor correct") + print(f"PASS: Battery voltage sensor correct") # Verify charge/discharge power sensor sensor_id = "sensor.predbat_solax_1618699116555534337_TP123456123123_charge_discharge_power" @@ -4875,7 +4875,7 @@ async def test_publish_device_realtime_data_main(): print(f"**** ERROR: Expected power 2500W, got {api2.dashboard_items[sensor_id]['state']} ****") failed = True else: - print(f"✓ Battery charge/discharge power sensor correct") + print(f"PASS: Battery charge/discharge power sensor correct") # Verify battery current sensor sensor_id = "sensor.predbat_solax_1618699116555534337_TP123456123123_battery_current" @@ -4886,7 +4886,7 @@ async def test_publish_device_realtime_data_main(): print(f"**** ERROR: Expected current 5.5A, got {api2.dashboard_items[sensor_id]['state']} ****") failed = True else: - print(f"✓ Battery current sensor correct") + print(f"PASS: Battery current sensor correct") # Verify battery temperature sensor sensor_id = "sensor.predbat_solax_1618699116555534337_TP123456123123_battery_temperature" @@ -4900,7 +4900,7 @@ async def test_publish_device_realtime_data_main(): print(f"**** ERROR: Wrong temperature unit ****") failed = True else: - print(f"✓ Battery temperature sensor correct") + print(f"PASS: Battery temperature sensor correct") # Test 3: Inverter with mpptMap instead of pvMap print("Test 3: Inverter with mpptMap instead of pvMap") @@ -4933,7 +4933,7 @@ async def test_publish_device_realtime_data_main(): print(f"**** ERROR: Expected PV power 1500 from mpptMap, got {api3.dashboard_items[sensor_id]['state']} ****") failed = True else: - print(f"✓ Inverter mpptMap PV power sensor correct") + print(f"PASS: Inverter mpptMap PV power sensor correct") # Test 4: Device with no pvMap or mpptMap print("Test 4: Device with no pvMap or mpptMap") @@ -4966,7 +4966,7 @@ async def test_publish_device_realtime_data_main(): print(f"**** ERROR: Expected PV power 0 (no map), got {api4.dashboard_items[sensor_id]['state']} ****") failed = True else: - print(f"✓ Inverter with no PV map defaults to 0") + print(f"PASS: Inverter with no PV map defaults to 0") # Test 5: Unknown device status codes print("Test 5: Unknown device status codes") @@ -4987,7 +4987,7 @@ async def test_publish_device_realtime_data_main(): print(f"**** ERROR: Expected 'Unknown Status', got {api5.dashboard_items[sensor_id]['state']} ****") failed = True else: - print(f"✓ Unknown device status handled correctly") + print(f"PASS: Unknown device status handled correctly") # Test 6: Empty realtime_device_data print("Test 6: Empty realtime_device_data") @@ -5005,7 +5005,7 @@ async def test_publish_device_realtime_data_main(): print(f"**** ERROR: Expected no sensors, got {len(api6.dashboard_items)} ****") failed = True else: - print(f"✓ Empty realtime_device_data test passed") + print(f"PASS: Empty realtime_device_data test passed") # Test 7: Multiple devices (inverter + battery) print("Test 7: Multiple devices (inverter + battery)") @@ -5036,7 +5036,7 @@ async def test_publish_device_realtime_data_main(): print(f"**** ERROR: Wrong battery SOC ****") failed = True else: - print(f"✓ Multiple devices test passed") + print(f"PASS: Multiple devices test passed") # Test 8: load_power is calculated correctly from inverter + battery data # load_power = pv - battery_charge_discharge - grid @@ -5084,7 +5084,7 @@ async def test_publish_device_realtime_data_main(): print(f"**** ERROR: Wrong load_power unit_of_measurement ****") failed = True else: - print(f"✓ load_power sensor calculated correctly ({expected_load}W)") + print(f"PASS: load_power sensor calculated correctly ({expected_load}W)") # Test 9: load_power with None battery power (API returns None for chargeDischargePower) # load_power should treat None as 0: load = pv - 0 - grid @@ -5128,7 +5128,7 @@ async def test_publish_device_realtime_data_main(): print(f"**** ERROR: Expected load_power {expected_load9}W with None battery, got {api9.dashboard_items[load_sensor9]['state']} ****") failed = True else: - print(f"✓ load_power with None chargeDischargePower treated as 0 ({expected_load9}W)") + print(f"PASS: load_power with None chargeDischargePower treated as 0 ({expected_load9}W)") # Test 10: Multi-inverter plant — PV and grid are aggregated; entity SN uses plant_inverters[0] # Plant has two inverters: INV_A (PV=1500W, grid=-200W) and INV_B (PV=500W, grid=-100W) @@ -5192,7 +5192,7 @@ async def test_publish_device_realtime_data_main(): print(f"**** ERROR: Expected aggregated load_power {expected_load10}W, got {api10.dashboard_items[load_sensor10]['state']} ****") failed = True else: - print(f"✓ Multi-inverter load_power aggregated correctly ({expected_load10}W) and tied to first inverter SN") + print(f"PASS: Multi-inverter load_power aggregated correctly ({expected_load10}W) and tied to first inverter SN") # Test 11: Battery SOH sensor published correctly, including zero-SOH guard and aggregate print("Test 11: Battery SOH sensor published correctly") @@ -5218,7 +5218,7 @@ async def test_publish_device_realtime_data_main(): print(f"**** ERROR: Wrong battery_soh unit ****") failed = True else: - print(f"✓ Per-device battery_soh sensor correct (0.92)") + print(f"PASS: Per-device battery_soh sensor correct (0.92)") # Per-inverter aggregate SOH sensor (updated in second pass) agg_soh_sensor = "sensor.predbat_solax_soh_plant_INV_SOH_battery_soh" @@ -5229,7 +5229,7 @@ async def test_publish_device_realtime_data_main(): print(f"**** ERROR: Expected aggregate battery_soh 0.92, got {api11.dashboard_items[agg_soh_sensor]['state']} ****") failed = True else: - print(f"✓ Aggregate battery_soh sensor correct (0.92)") + print(f"PASS: Aggregate battery_soh sensor correct (0.92)") # Verify batterySOH=0 is treated as 1.0 (guard against bad API data) api11b = MockSolaxAPI() @@ -5250,10 +5250,10 @@ async def test_publish_device_realtime_data_main(): print(f"**** ERROR: Expected battery_soh 1.0 for batterySOH=0, got {api11b.dashboard_items[zero_soh_sensor]['state']} ****") failed = True else: - print(f"✓ batterySOH=0 correctly guarded to 1.0") + print(f"PASS: batterySOH=0 correctly guarded to 1.0") if not failed: - print("✓ publish_device_realtime_data tests passed") + print("PASS: publish_device_realtime_data tests passed") return failed @@ -5278,7 +5278,7 @@ async def test_helper_methods_main(): print(f"**** ERROR: Expected 10000W, got {result}W ****") failed = True else: - print(f"✓ Single inverter power calculation correct (10000W)") + print(f"PASS: Single inverter power calculation correct (10000W)") # Test 2: get_max_power_inverter - multiple inverters print("Test 2: get_max_power_inverter - multiple inverters") @@ -5295,7 +5295,7 @@ async def test_helper_methods_main(): print(f"**** ERROR: Expected 23500W, got {result2}W ****") failed = True else: - print(f"✓ Multiple inverter power calculation correct (23500W)") + print(f"PASS: Multiple inverter power calculation correct (23500W)") # Test 3: get_max_power_inverter - no inverters print("Test 3: get_max_power_inverter - no inverters") @@ -5307,7 +5307,7 @@ async def test_helper_methods_main(): print(f"**** ERROR: Expected 0W for no inverters, got {result3}W ****") failed = True else: - print(f"✓ No inverters returns 0W") + print(f"PASS: No inverters returns 0W") # Test 4: get_max_power_battery - single battery print("Test 4: get_max_power_battery - single battery") @@ -5322,7 +5322,7 @@ async def test_helper_methods_main(): print(f"**** ERROR: Expected 5000W, got {result4}W ****") failed = True else: - print(f"✓ Single battery power calculation correct (5000W)") + print(f"PASS: Single battery power calculation correct (5000W)") # Test 5: get_max_power_battery - multiple batteries print("Test 5: get_max_power_battery - multiple batteries") @@ -5338,7 +5338,7 @@ async def test_helper_methods_main(): print(f"**** ERROR: Expected 10000W, got {result5}W ****") failed = True else: - print(f"✓ Multiple battery power calculation correct (10000W)") + print(f"PASS: Multiple battery power calculation correct (10000W)") # Test 6: get_max_power_battery - fallback to inverter power print("Test 6: get_max_power_battery - fallback to inverter power") @@ -5354,7 +5354,7 @@ async def test_helper_methods_main(): print(f"**** ERROR: Expected 12000W (inverter fallback), got {result6}W ****") failed = True else: - print(f"✓ Battery power fallback to inverter correct (12000W)") + print(f"PASS: Battery power fallback to inverter correct (12000W)") # Test 7: get_max_soc_battery print("Test 7: get_max_soc_battery") @@ -5368,7 +5368,7 @@ async def test_helper_methods_main(): print(f"**** ERROR: Expected 15.0 kWh, got {result7} kWh ****") failed = True else: - print(f"✓ Max SOC battery correct (15.0 kWh)") + print(f"PASS: Max SOC battery correct (15.0 kWh)") # Test 8: get_current_soc_battery_kwh - single battery print("Test 8: get_current_soc_battery_kwh - single battery") @@ -5385,7 +5385,7 @@ async def test_helper_methods_main(): print(f"**** ERROR: Expected {expected8} kWh, got {result8} kWh ****") failed = True else: - print(f"✓ Single battery current SOC correct (11.25 kWh)") + print(f"PASS: Single battery current SOC correct (11.25 kWh)") # Test 9: get_current_soc_battery_kwh - multiple batteries (average) print("Test 9: get_current_soc_battery_kwh - multiple batteries (average)") @@ -5403,7 +5403,7 @@ async def test_helper_methods_main(): print(f"**** ERROR: Expected {expected9} kWh, got {result9} kWh ****") failed = True else: - print(f"✓ Multiple battery current SOC average correct (14.0 kWh)") + print(f"PASS: Multiple battery current SOC average correct (14.0 kWh)") # Test 10: get_current_soc_battery_kwh - no batteries print("Test 10: get_current_soc_battery_kwh - no batteries") @@ -5415,7 +5415,7 @@ async def test_helper_methods_main(): print(f"**** ERROR: Expected 0 kWh for no batteries, got {result10} kWh ****") failed = True else: - print(f"✓ No batteries returns 0 kWh") + print(f"PASS: No batteries returns 0 kWh") # Test 11: get_battery_temperature - single battery print("Test 11: get_battery_temperature - single battery") @@ -5430,7 +5430,7 @@ async def test_helper_methods_main(): print(f"**** ERROR: Expected 22.5°C, got {result11}°C ****") failed = True else: - print(f"✓ Single battery temperature correct (22.5°C)") + print(f"PASS: Single battery temperature correct (22.5°C)") # Test 12: get_battery_temperature - multiple batteries (minimum) print("Test 12: get_battery_temperature - multiple batteries (minimum)") @@ -5447,7 +5447,7 @@ async def test_helper_methods_main(): print(f"**** ERROR: Expected minimum temperature 18.5°C, got {result12}°C ****") failed = True else: - print(f"✓ Multiple battery temperature minimum correct (18.5°C)") + print(f"PASS: Multiple battery temperature minimum correct (18.5°C)") # Test 13: get_battery_temperature - no temperature data print("Test 13: get_battery_temperature - no temperature data") @@ -5462,7 +5462,7 @@ async def test_helper_methods_main(): print(f"**** ERROR: Expected None for no temperature data, got {result13}°C ****") failed = True else: - print(f"✓ No temperature data returns None") + print(f"PASS: No temperature data returns None") # Test 14: get_charge_discharge_power_battery - single battery print("Test 14: get_charge_discharge_power_battery - single battery") @@ -5477,7 +5477,7 @@ async def test_helper_methods_main(): print(f"**** ERROR: Expected 2500W, got {result14}W ****") failed = True else: - print(f"✓ Single battery charge/discharge power correct (2500W)") + print(f"PASS: Single battery charge/discharge power correct (2500W)") # Test 15: get_charge_discharge_power_battery - multiple batteries (sum) print("Test 15: get_charge_discharge_power_battery - multiple batteries (sum)") @@ -5493,10 +5493,10 @@ async def test_helper_methods_main(): print(f"**** ERROR: Expected -500W, got {result15}W ****") failed = True else: - print(f"✓ Multiple battery charge/discharge power sum correct (-500W)") + print(f"PASS: Multiple battery charge/discharge power sum correct (-500W)") if not failed: - print("✓ helper methods tests passed") + print("PASS: helper methods tests passed") return failed @@ -5538,7 +5538,7 @@ async def test_automatic_config_main(): print(f"**** ERROR: battery_min_soc should be [{SOLAX_MIN_RESERVE_PERCENT}] so Predbat's reserve logic never targets below what SolaX allows, got {api.get_arg('battery_min_soc')} ****") failed = True else: - print(f"✓ Single plant configuration correct") + print(f"PASS: Single plant configuration correct") # Test 2: Multiple plants with inverters and batteries print("Test 2: Multiple plants with inverters and batteries") @@ -5568,7 +5568,7 @@ async def test_automatic_config_main(): print(f"**** ERROR: battery_min_soc should have one entry per plant, got {api2.get_arg('battery_min_soc')} ****") failed = True else: - print(f"✓ Multiple plant configuration correct") + print(f"PASS: Multiple plant configuration correct") # Test 3: Plant with inverter but no battery (should be skipped) print("Test 3: Plant with inverter but no battery (should be skipped)") @@ -5591,7 +5591,7 @@ async def test_automatic_config_main(): print(f"**** ERROR: Expected num_inverters=1 (only plant with battery), got {api3.get_arg('num_inverters')} ****") failed = True else: - print(f"✓ Plant without battery correctly skipped") + print(f"PASS: Plant without battery correctly skipped") # Test 4: No plants with both inverter and battery (should raise error) print("Test 4: No plants with both inverter and battery (should raise error)") @@ -5607,7 +5607,7 @@ async def test_automatic_config_main(): failed = True except ValueError as e: if "No plants with inverters and batteries found" in str(e): - print(f"✓ Correctly raised ValueError for no valid plants") + print(f"PASS: Correctly raised ValueError for no valid plants") else: print(f"**** ERROR: Wrong error message: {e} ****") failed = True @@ -5627,13 +5627,13 @@ async def test_automatic_config_main(): # Entity names should use plant ID directly battery_power_entity = api5.get_arg("battery_power") if battery_power_entity and "Test_Plant_123" in battery_power_entity[0]: - print(f"✓ Entity names correctly use plant ID") + print(f"PASS: Entity names correctly use plant ID") else: print(f"**** ERROR: Entity name incorrect: {battery_power_entity} ****") failed = True if not failed: - print("✓ automatic_config tests passed") + print("PASS: automatic_config tests passed") return failed @@ -5695,7 +5695,7 @@ async def test_publish_controls_main(): print(f"**** ERROR: Reserve incorrect: {api.dashboard_items[reserve]['state']} ****") failed = True else: - print(f"✓ Control entities created correctly") + print(f"PASS: Control entities created correctly") # Test 2: Verify export controls print("Test 2: Verify export controls") @@ -5715,7 +5715,7 @@ async def test_publish_controls_main(): print(f"**** ERROR: Export rate incorrect: {api.dashboard_items[export_rate]['state']} ****") failed = True else: - print(f"✓ Export control entities correct") + print(f"PASS: Export control entities correct") # Test 3: Multiple plants print("Test 3: Multiple plants") @@ -5752,7 +5752,7 @@ async def test_publish_controls_main(): print(f"**** ERROR: Plant B reserve incorrect ****") failed = True else: - print(f"✓ Multiple plant controls correct") + print(f"PASS: Multiple plant controls correct") # Test 4: Verify attributes (min, max, units, options) print("Test 4: Verify attributes") @@ -5770,7 +5770,7 @@ async def test_publish_controls_main(): print(f"**** ERROR: Target SOC units incorrect: {attrs.get('unit_of_measurement')} ****") failed = True else: - print(f"✓ Entity attributes correct") + print(f"PASS: Entity attributes correct") # Test 5: Verify time options print("Test 5: Verify time options") @@ -5788,10 +5788,10 @@ async def test_publish_controls_main(): print(f"**** ERROR: Start time options count incorrect: {len(attrs.get('options', []))} ****") failed = True else: - print(f"✓ Time options correct") + print(f"PASS: Time options correct") if not failed: - print("✓ publish_controls tests passed") + print("PASS: publish_controls tests passed") return failed @@ -5838,7 +5838,7 @@ async def test_run_main(): print(f"**** ERROR: query_plant_info not called on first run ****") failed = True else: - print(f"✓ First run initialization correct") + print(f"PASS: First run initialization correct") # Test 2: Subsequent run (no first-time actions) print("Test 2: Subsequent run (no first-time actions)") @@ -5867,7 +5867,7 @@ async def test_run_main(): print(f"**** ERROR: query_device_info should not be called at 2 minutes ****") failed = True else: - print(f"✓ Subsequent run correct") + print(f"PASS: Subsequent run correct") # Test 3: 60-second cycle (realtime data refresh) print("Test 3: 60-second cycle (realtime data refresh)") @@ -5898,7 +5898,7 @@ async def test_run_main(): print(f"**** ERROR: publish methods not called at 60 seconds ****") failed = True else: - print(f"✓ 60-second cycle correct") + print(f"PASS: 60-second cycle correct") # Test 4: 30-minute cycle (device info refresh) print("Test 4: 30-minute cycle (device info refresh)") @@ -5926,7 +5926,7 @@ async def test_run_main(): print(f"**** ERROR: query_device_info not called enough times (expected 2+, got {mock_device.call_count}) ****") failed = True else: - print(f"✓ 30-minute cycle correct") + print(f"PASS: 30-minute cycle correct") # Test 5: Read-only mode (controls disabled) print("Test 5: Read-only mode (controls disabled)") @@ -5955,7 +5955,7 @@ async def test_run_main(): print(f"**** ERROR: apply_controls called in read-only mode ****") failed = True else: - print(f"✓ Read-only mode correct") + print(f"PASS: Read-only mode correct") # Test 6: Automatic config on first run print("Test 6: Automatic config on first run") @@ -5985,7 +5985,7 @@ async def test_run_main(): print(f"**** ERROR: automatic_config not called when automatic=True on first run ****") failed = True else: - print(f"✓ Automatic config triggered correctly") + print(f"PASS: Automatic config triggered correctly") # Test 7: Failed plant info fetch print("Test 7: Failed plant info fetch") @@ -6000,10 +6000,10 @@ async def test_run_main(): print(f"**** ERROR: Should return False when plant_info is None ****") failed = True else: - print(f"✓ Failed plant info handled correctly") + print(f"PASS: Failed plant info handled correctly") if not failed: - print("✓ run loop tests passed") + print("PASS: run loop tests passed") return failed @@ -6050,7 +6050,7 @@ async def test_set_default_work_mode_main(): print(f"**** ERROR: Expected charge_upper_soc 100, got {call_args[0][3]} ****") failed = True else: - print(f"✓ Successful call invokes set_work_mode with correct parameters") + print(f"PASS: Successful call invokes set_work_mode with correct parameters") # Test 2: Failed set_work_mode should not set flag print("Test 2: Failed set_work_mode should not set flag") @@ -6066,7 +6066,7 @@ async def test_set_default_work_mode_main(): print(f"**** ERROR: Should return False when set_work_mode fails ****") failed = True else: - print(f"✓ Failed set_work_mode correctly does not set flag") + print(f"PASS: Failed set_work_mode correctly does not set flag") # Test 3: Verify success log messages print("Test 3: Verify success log messages") @@ -6084,7 +6084,7 @@ async def test_set_default_work_mode_main(): print(f"**** ERROR: Success log message not found ****") failed = True else: - print(f"✓ Success log message generated correctly") + print(f"PASS: Success log message generated correctly") # Test 4: Failed call should generate warning log print("Test 4: Failed call should generate warning log") @@ -6102,7 +6102,7 @@ async def test_set_default_work_mode_main(): print(f"**** ERROR: Warning log message not found ****") failed = True else: - print(f"✓ Warning log message generated correctly") + print(f"PASS: Warning log message generated correctly") # Test 5: Verify business_type parameter is passed through print("Test 5: Verify business_type parameter is passed through") @@ -6120,7 +6120,7 @@ async def test_set_default_work_mode_main(): print(f"**** ERROR: Expected business_type=4, got {call_kwargs.get('business_type')} ****") failed = True else: - print(f"✓ business_type parameter passed through correctly") + print(f"PASS: business_type parameter passed through correctly") # Test 6: Verify mode parameter is passed through (backup mode) print("Test 6: Verify mode parameter is passed through (backup mode)") @@ -6138,10 +6138,10 @@ async def test_set_default_work_mode_main(): print(f"**** ERROR: Expected mode 'backup', got {call_args6[0][0]} ****") failed = True else: - print(f"✓ mode parameter passed through correctly") + print(f"PASS: mode parameter passed through correctly") if not failed: - print("✓ set_default_work_mode tests passed") + print("PASS: set_default_work_mode tests passed") return failed @@ -6199,7 +6199,7 @@ async def test_positive_or_negative_mode_main(): print(f"**** ERROR: Wrong command_name: {command_name} ****") failed = True else: - print(f"✓ Charge mode parameters correct") + print(f"PASS: Charge mode parameters correct") # Test 2: Discharge mode (positive battery_power) print("Test 2: Discharge mode (positive battery_power)") @@ -6221,7 +6221,7 @@ async def test_positive_or_negative_mode_main(): print(f"**** ERROR: Wrong battery_power for discharge: {payload2.get('batteryPower')} ****") failed = True else: - print(f"✓ Discharge mode parameters correct") + print(f"PASS: Discharge mode parameters correct") # Test 3: Default next_motion parameter print("Test 3: Default next_motion parameter") @@ -6244,7 +6244,7 @@ async def test_positive_or_negative_mode_main(): print(f"**** ERROR: Default next_motion should be 161, got {payload3.get('nextMotion')} ****") failed = True else: - print(f"✓ Default next_motion (161) correct") + print(f"PASS: Default next_motion (161) correct") # Test 4: Custom next_motion parameter (Exit Remote Control) print("Test 4: Custom next_motion parameter (Exit Remote Control)") @@ -6266,7 +6266,7 @@ async def test_positive_or_negative_mode_main(): print(f"**** ERROR: next_motion should be 160, got {payload4.get('nextMotion')} ****") failed = True else: - print(f"✓ Custom next_motion (160) correct") + print(f"PASS: Custom next_motion (160) correct") # Test 5: Default business_type (residential) print("Test 5: Default business_type (residential)") @@ -6289,7 +6289,7 @@ async def test_positive_or_negative_mode_main(): print(f"**** ERROR: Default business_type should be 1, got {payload5.get('businessType')} ****") failed = True else: - print(f"✓ Default business_type (residential) correct") + print(f"PASS: Default business_type (residential) correct") # Test 6: Custom business_type (commercial) print("Test 6: Custom business_type (commercial)") @@ -6311,7 +6311,7 @@ async def test_positive_or_negative_mode_main(): print(f"**** ERROR: business_type should be 4 (commercial), got {payload6.get('businessType')} ****") failed = True else: - print(f"✓ Custom business_type (commercial) correct") + print(f"PASS: Custom business_type (commercial) correct") # Test 7: Failed command execution print("Test 7: Failed command execution") @@ -6327,7 +6327,7 @@ async def test_positive_or_negative_mode_main(): print(f"**** ERROR: Should return False on failure ****") failed = True else: - print(f"✓ Failed command execution handled correctly") + print(f"PASS: Failed command execution handled correctly") # Test 8: Zero battery power (edge case) print("Test 8: Zero battery power (edge case)") @@ -6349,7 +6349,7 @@ async def test_positive_or_negative_mode_main(): print(f"**** ERROR: Zero battery_power should be preserved, got {payload8.get('batteryPower')} ****") failed = True else: - print(f"✓ Zero battery power handled correctly") + print(f"PASS: Zero battery power handled correctly") # Test 9: Large duration value print("Test 9: Large duration value") @@ -6371,10 +6371,10 @@ async def test_positive_or_negative_mode_main(): print(f"**** ERROR: Duration should be 43200, got {payload9.get('timeOfDuration')} ****") failed = True else: - print(f"✓ Large duration value handled correctly") + print(f"PASS: Large duration value handled correctly") if not failed: - print("✓ positive_or_negative_mode tests passed") + print("PASS: positive_or_negative_mode tests passed") return failed @@ -6432,7 +6432,7 @@ async def test_self_consume_charge_only_mode_main(): print(f"**** ERROR: Wrong sn_list passed to send_command_and_wait: {sn_list} ****") failed = True else: - print(f"✓ Single inverter parameters correct") + print(f"PASS: Single inverter parameters correct") # Test 2: Multiple inverters print("Test 2: Multiple inverters") @@ -6454,7 +6454,7 @@ async def test_self_consume_charge_only_mode_main(): print(f"**** ERROR: Wrong snList for multiple inverters: {payload2.get('snList')} ****") failed = True else: - print(f"✓ Multiple inverters handled correctly") + print(f"PASS: Multiple inverters handled correctly") # Test 3: Default next_motion parameter (Back to Self-Consume) print("Test 3: Default next_motion parameter (Back to Self-Consume)") @@ -6477,7 +6477,7 @@ async def test_self_consume_charge_only_mode_main(): print(f"**** ERROR: Default next_motion should be 161 (Back to Self-Consume), got {payload3.get('nextMotion')} ****") failed = True else: - print(f"✓ Default next_motion (161) correct") + print(f"PASS: Default next_motion (161) correct") # Test 4: Custom next_motion (Exit Remote Control) print("Test 4: Custom next_motion (Exit Remote Control)") @@ -6499,7 +6499,7 @@ async def test_self_consume_charge_only_mode_main(): print(f"**** ERROR: next_motion should be 160 (Exit Remote Control), got {payload4.get('nextMotion')} ****") failed = True else: - print(f"✓ Custom next_motion (160) correct") + print(f"PASS: Custom next_motion (160) correct") # Test 5: Default business_type (residential) print("Test 5: Default business_type (residential)") @@ -6522,7 +6522,7 @@ async def test_self_consume_charge_only_mode_main(): print(f"**** ERROR: Default business_type should be 1 (residential), got {payload5.get('businessType')} ****") failed = True else: - print(f"✓ Default business_type (residential) correct") + print(f"PASS: Default business_type (residential) correct") # Test 6: Custom business_type (commercial) print("Test 6: Custom business_type (commercial)") @@ -6544,7 +6544,7 @@ async def test_self_consume_charge_only_mode_main(): print(f"**** ERROR: business_type should be 4 (commercial), got {payload6.get('businessType')} ****") failed = True else: - print(f"✓ Custom business_type (commercial) correct") + print(f"PASS: Custom business_type (commercial) correct") # Test 7: Failed command execution print("Test 7: Failed command execution") @@ -6560,7 +6560,7 @@ async def test_self_consume_charge_only_mode_main(): print(f"**** ERROR: Should return False on failure ****") failed = True else: - print(f"✓ Failed command execution handled correctly") + print(f"PASS: Failed command execution handled correctly") # Test 8: Short duration (edge case) print("Test 8: Short duration (edge case)") @@ -6582,7 +6582,7 @@ async def test_self_consume_charge_only_mode_main(): print(f"**** ERROR: Short duration should be preserved, got {payload8.get('timeOfDuration')} ****") failed = True else: - print(f"✓ Short duration handled correctly") + print(f"PASS: Short duration handled correctly") # Test 9: Long duration (edge case) print("Test 9: Long duration (edge case)") @@ -6604,7 +6604,7 @@ async def test_self_consume_charge_only_mode_main(): print(f"**** ERROR: Long duration should be preserved, got {payload9.get('timeOfDuration')} ****") failed = True else: - print(f"✓ Long duration handled correctly") + print(f"PASS: Long duration handled correctly") # Test 10: Empty inverter list (edge case) print("Test 10: Empty inverter list (edge case)") @@ -6626,10 +6626,10 @@ async def test_self_consume_charge_only_mode_main(): print(f"**** ERROR: Empty list should be preserved, got {payload10.get('snList')} ****") failed = True else: - print(f"✓ Empty inverter list handled correctly") + print(f"PASS: Empty inverter list handled correctly") if not failed: - print("✓ self_consume_charge_only_mode tests passed") + print("PASS: self_consume_charge_only_mode tests passed") return failed @@ -6675,7 +6675,7 @@ async def test_query_request_result_main(): print(f"**** ERROR: Wrong requestId: {json_data.get('requestId')} ****") failed = True else: - print(f"✓ Single device success status returned correctly") + print(f"PASS: Single device success status returned correctly") # Test 2: Multiple devices - all successful print("Test 2: Multiple devices - all successful") @@ -6691,7 +6691,7 @@ async def test_query_request_result_main(): print(f"**** ERROR: Should return status 4 for all successful, got {result2} ****") failed = True else: - print(f"✓ Multiple devices all successful") + print(f"PASS: Multiple devices all successful") # Test 3: Multiple devices - one device offline print("Test 3: Multiple devices - one device offline") @@ -6707,7 +6707,7 @@ async def test_query_request_result_main(): print(f"**** ERROR: Should return first non-success status 2, got {result3} ****") failed = True else: - print(f"✓ Non-success status detected correctly") + print(f"PASS: Non-success status detected correctly") # Test 4: Empty result array print("Test 4: Empty result array") @@ -6723,7 +6723,7 @@ async def test_query_request_result_main(): print(f"**** ERROR: Should return default status 4 for empty result, got {result4} ****") failed = True else: - print(f"✓ Empty result array handled correctly") + print(f"PASS: Empty result array handled correctly") # Test 5: API error response print("Test 5: API error response") @@ -6739,7 +6739,7 @@ async def test_query_request_result_main(): print(f"**** ERROR: Should return None on API error, got {result5} ****") failed = True else: - print(f"✓ API error handled correctly") + print(f"PASS: API error handled correctly") # Test 6: request_get returns None (network error) print("Test 6: request_get returns None (network error)") @@ -6755,7 +6755,7 @@ async def test_query_request_result_main(): print(f"**** ERROR: Should return None on network error, got {result6} ****") failed = True else: - print(f"✓ Network error handled correctly") + print(f"PASS: Network error handled correctly") # Test 7: Missing result field in response print("Test 7: Missing result field in response") @@ -6774,7 +6774,7 @@ async def test_query_request_result_main(): print(f"**** ERROR: Should return default status 4 when result missing, got {result7} ****") failed = True else: - print(f"✓ Missing result field handled correctly") + print(f"PASS: Missing result field handled correctly") # Test 8: First device fails, others succeed print("Test 8: First device fails, others succeed") @@ -6790,7 +6790,7 @@ async def test_query_request_result_main(): print(f"**** ERROR: Should return first device status 1, got {result8} ****") failed = True else: - print(f"✓ First device failure detected correctly") + print(f"PASS: First device failure detected correctly") # Test 9: Device result with missing fields print("Test 9: Device result with missing fields") @@ -6807,7 +6807,7 @@ async def test_query_request_result_main(): print(f"**** ERROR: Should return None when device has missing status field ****") failed = True else: - print(f"✓ Missing status field returns None correctly") + print(f"PASS: Missing status field returns None correctly") # Test 10: Long request ID print("Test 10: Long request ID") @@ -6830,10 +6830,10 @@ async def test_query_request_result_main(): print(f"**** ERROR: Long request ID not preserved ****") failed = True else: - print(f"✓ Long request ID handled correctly") + print(f"PASS: Long request ID handled correctly") if not failed: - print("✓ query_request_result tests passed") + print("PASS: query_request_result tests passed") return failed @@ -6899,7 +6899,7 @@ def mock_get_state_text(entity_id, default=None): print(f"**** ERROR: Expected reserve 20, got {api.controls[plant_id]['reserve']} ****") failed = True else: - print(f"✓ Text values converted to numbers correctly") + print(f"PASS: Text values converted to numbers correctly") # Test 2: Values bounded by min/max print("Test 2: Values bounded by min/max") @@ -6938,7 +6938,7 @@ def mock_get_state_out_of_range(entity_id, default=None): print(f"**** ERROR: Expected charge_rate floored at 0, got {api2.controls[plant_id]['charge']['rate']} ****") failed = True else: - print(f"✓ Out-of-range values bounded correctly") + print(f"PASS: Out-of-range values bounded correctly") # Test 3: Default values used when state not available print("Test 3: Default values used when state not available") @@ -6970,7 +6970,7 @@ def mock_get_state_none(entity_id, default=None): print(f"**** ERROR: Expected default reserve 10, got {api3.controls[plant_id]['reserve']} ****") failed = True else: - print(f"✓ Default values used correctly") + print(f"PASS: Default values used correctly") # Test 4: Invalid text values fall back to defaults print("Test 4: Invalid text values fall back to defaults") @@ -7000,7 +7000,7 @@ def mock_get_state_invalid(entity_id, default=None): print(f"**** ERROR: Expected default reserve 10 for invalid input, got {api4.controls[plant_id]['reserve']} ****") failed = True else: - print(f"✓ Invalid text values fall back to defaults") + print(f"PASS: Invalid text values fall back to defaults") # Test 5: Mixed valid numeric and text values print("Test 5: Mixed valid numeric and text values") @@ -7039,7 +7039,7 @@ def mock_get_state_mixed(entity_id, default=None): print(f"**** ERROR: Expected reserve 15, got {api5.controls[plant_id]['reserve']} ****") failed = True else: - print(f"✓ Mixed numeric and text values handled correctly") + print(f"PASS: Mixed numeric and text values handled correctly") # Test 6: Switch entities return "on"/"off" strings from HA — must be normalised to bool print("Test 6: Switch 'on'/'off' strings normalised to bool") @@ -7066,7 +7066,7 @@ def mock_get_state_switch_strings(entity_id, default=None): print(f"**** ERROR: Expected export enable False from 'off' string, got {api6.controls[plant_id]['export']['enable']!r} ****") failed = True else: - print("✓ Switch 'on'/'off' strings normalised to bool correctly") + print("PASS: Switch 'on'/'off' strings normalised to bool correctly") # Test 7: Switch entities return uppercase "ON"/"OFF" — case-insensitive normalisation print("Test 7: Switch uppercase 'ON'/'OFF' strings normalised to bool") @@ -7093,10 +7093,10 @@ def mock_get_state_switch_upper(entity_id, default=None): print(f"**** ERROR: Expected export enable False from 'OFF' string, got {api7.controls[plant_id]['export']['enable']!r} ****") failed = True else: - print("✓ Uppercase switch strings normalised correctly") + print("PASS: Uppercase switch strings normalised correctly") if not failed: - print("✓ fetch_controls value conversion tests passed") + print("PASS: fetch_controls value conversion tests passed") return failed @@ -7147,7 +7147,7 @@ async def test_exit_vpp_mode_main(): print(f"**** ERROR: Wrong sn_list passed: {sn_list} ****") failed = True else: - print(f"✓ Single inverter exit VPP mode correct") + print(f"PASS: Single inverter exit VPP mode correct") # Test 2: Multiple inverters print("Test 2: Multiple inverters") @@ -7169,7 +7169,7 @@ async def test_exit_vpp_mode_main(): print(f"**** ERROR: Wrong snList for multiple inverters: {payload2.get('snList')} ****") failed = True else: - print(f"✓ Multiple inverters handled correctly") + print(f"PASS: Multiple inverters handled correctly") # Test 3: Default business_type (residential) print("Test 3: Default business_type (residential)") @@ -7191,7 +7191,7 @@ async def test_exit_vpp_mode_main(): print(f"**** ERROR: Default business_type should be 1, got {payload3.get('businessType')} ****") failed = True else: - print(f"✓ Default business_type (residential) correct") + print(f"PASS: Default business_type (residential) correct") # Test 4: Custom business_type (commercial) print("Test 4: Custom business_type (commercial)") @@ -7213,7 +7213,7 @@ async def test_exit_vpp_mode_main(): print(f"**** ERROR: business_type should be 4 (commercial), got {payload4.get('businessType')} ****") failed = True else: - print(f"✓ Custom business_type (commercial) correct") + print(f"PASS: Custom business_type (commercial) correct") # Test 5: Failed command execution print("Test 5: Failed command execution") @@ -7229,7 +7229,7 @@ async def test_exit_vpp_mode_main(): print(f"**** ERROR: Should return False on failure ****") failed = True else: - print(f"✓ Failed command execution handled correctly") + print(f"PASS: Failed command execution handled correctly") # Test 6: Maximum 10 devices print("Test 6: Maximum 10 devices") @@ -7252,7 +7252,7 @@ async def test_exit_vpp_mode_main(): print(f"**** ERROR: Expected 10 devices, got {len(payload6.get('snList'))} ****") failed = True else: - print(f"✓ Maximum 10 devices handled correctly") + print(f"PASS: Maximum 10 devices handled correctly") # Test 7: Empty list (edge case) print("Test 7: Empty list (edge case)") @@ -7274,9 +7274,9 @@ async def test_exit_vpp_mode_main(): print(f"**** ERROR: Empty list should be preserved, got {payload7.get('snList')} ****") failed = True else: - print(f"✓ Empty list handled correctly") + print(f"PASS: Empty list handled correctly") if not failed: - print("✓ exit_vpp_mode tests passed") + print("PASS: exit_vpp_mode tests passed") return failed diff --git a/apps/predbat/tests/test_solcast.py b/apps/predbat/tests/test_solcast.py index 3fd294ad9..9abf4b67d 100644 --- a/apps/predbat/tests/test_solcast.py +++ b/apps/predbat/tests/test_solcast.py @@ -1120,7 +1120,7 @@ def test_forecast_solar_rate_limit_suppresses_fetch(my_predbat): } ] - # Step 1: first call gets a 429 → rate limit should be stored + # Step 1: first call gets a 429 -> rate limit should be stored test_api.set_mock_response("forecast.solar", {"error": "rate limit"}, 429) def create_mock_session(*args, **kwargs): @@ -2345,7 +2345,7 @@ def mock_minute_data_import_export(max_days_previous, now_utc, key, scale=1.0, r base.minute_data_import_export = mock_minute_data_import_export - # No forecast history → enabled_calibration will be False (< 3 days), but power + # No forecast history -> enabled_calibration will be False (< 3 days), but power # conversion and capped_data paths still execute. solar.get_history_wrapper = lambda entity_id, days, required=False: [] @@ -2477,7 +2477,7 @@ def make_h0_ha_history(now_utc, days_back): """ Build HA-format h0 forecast history (kW sensor) spanning days_back days. Returns 1.0 kW forecast during 10:00-11:00 UTC for each past day so that - the oldest timestamp is days_back days ago → pv_forecast_hist_days = days_back. + the oldest timestamp is days_back days ago -> pv_forecast_hist_days = days_back. """ entries = [] for day in range(days_back, 0, -1): @@ -2543,7 +2543,7 @@ def mock_get_history(entity_id, days, required=False, _h0=h0_ha_history): total = solar.pv_calibration_total_adjustment if not expect_enabled: - # 2-day history → calibration disabled → hard-coded defaults must be used + # 2-day history -> calibration disabled -> hard-coded defaults must be used if abs(worst - 0.7) > 0.001: print("ERROR: {}-day history (disabled): worst_scaling should be 0.7, got {}".format(days_back, worst)) failed = True @@ -2554,8 +2554,8 @@ def mock_get_history(entity_id, days, required=False, _h0=h0_ha_history): print("ERROR: {}-day history (disabled): total_adjustment should be 1.0, got {}".format(days_back, total)) failed = True else: - # 5-day history → calibration enabled → must NOT produce the disabled-default values - # (actual 0.5 kWh/day < forecast 1.0 kWh/day → worst and best both < 1.0, not 0.7/1.3) + # 5-day history -> calibration enabled -> must NOT produce the disabled-default values + # (actual 0.5 kWh/day < forecast 1.0 kWh/day -> worst and best both < 1.0, not 0.7/1.3) if abs(worst - 0.7) < 0.001: print("ERROR: {}-day history (enabled): worst_scaling is 0.7 (disabled default) – calibration appears disabled".format(days_back)) failed = True @@ -2665,7 +2665,7 @@ def test_pv_calibration_no_history_not_zeroed(my_predbat): base = test_api.mock_base plan_interval = base.plan_interval_minutes # 5 - # No historical actual production and no forecast history at all → no valid days, + # No historical actual production and no forecast history at all -> no valid days, # so max_pv_power_hist = max_pv_power_forecast = 0 and calibration is disabled. def mock_minute_data_import_export(max_days_previous, now_utc, key, scale=1.0, required_unit=None, increment=True, smoothing=True, pad=True): return {} @@ -2720,7 +2720,7 @@ def test_pv_calibration_synthetic_values(my_predbat): - average_day_scaling ≈ 0.5 - worst / best day scaling = 1.0 (no day-to-day variance) - calibrated gen-slot minute ≈ 0.5 × input - - pv_estimate10 / pv_estimate90 ≈ pv_estimateCL (worst=best=1.0 → no spread) + - pv_estimate10 / pv_estimate90 ≈ pv_estimateCL (worst=best=1.0 -> no spread) Sub-case B – variable performance (3 days: actual = 0.5, 1.0, 1.5 kWh each, forecast=1.0): - average_day_scaling ≈ 0.963 (weighted: (0.5×1.0 + 1.0×0.9 + 1.5×0.8) / 2.7 ≈ 0.963) @@ -2794,7 +2794,7 @@ def run_scenario(actual_per_day): hist = build_pv_today_hist(actual_per_day) # Build pv_forecast minute dict directly (bypass full h0 pipeline). - # pv_calibration maps minute N → minute_absolute = minutes_now - N. + # pv_calibration maps minute N -> minute_absolute = minutes_now - N. # For day D's gen window (GEN_START..GEN_END-1 min-of-day): # minute_ago = D*1440 + (minutes_now - m_of_day) # We provide FORECAST_KW kW at each gen-window minute for all past days. @@ -2866,8 +2866,8 @@ def mock_minute_import_export(max_days_prev, now_utc, key, scale=1.0, required_u print("ERROR [A]: adj_minute[630] / raw = {:.4f}, expected ~{:.4f} (total_adj ± 15%)".format(ratio, r["total_adj"])) failed = True - # pv_estimate10 should use worst scaling (=1.0) → equal to pv_estimateCL. - # pv_estimate90 should use best scaling (=1.0) → equal to pv_estimateCL too (no spread). + # pv_estimate10 should use worst scaling (=1.0) -> equal to pv_estimateCL. + # pv_estimate90 should use best scaling (=1.0) -> equal to pv_estimateCL too (no spread). for entry in r["adj_data"]: cl = entry.get("pv_estimateCL") e10 = entry.get("pv_estimate10") @@ -2882,7 +2882,7 @@ def mock_minute_import_export(max_days_prev, now_utc, key, scale=1.0, required_u failed = True break - # --- Sub-case B: 3 days at 0.5x, 1.0x, 1.5x → weighted avg=0.963, worst=0.519, best=1.558 --- + # --- Sub-case B: 3 days at 0.5x, 1.0x, 1.5x -> weighted avg=0.963, worst=0.519, best=1.558 --- r = run_scenario([0.5, 1.0, 1.5]) if abs(r["total_adj"] - 0.963) > TOL: @@ -3172,7 +3172,7 @@ def test_pv_calibration_60min_period(my_predbat): base = test_api.mock_base base.plan_interval_minutes = PLAN_INTERVAL - # Flat historical production matching the forecast → calibration ratio ~1.0 + # Flat historical production matching the forecast -> calibration ratio ~1.0 # so the calibrated values should be very close to the raw forecast values. hist = {} days = 5 @@ -3300,7 +3300,7 @@ def test_pv_calibration_15min_period(my_predbat): base = test_api.mock_base base.plan_interval_minutes = PLAN_INTERVAL - # Flat historical production matching the forecast → calibration ratio ~1.0 + # Flat historical production matching the forecast -> calibration ratio ~1.0 hist = {} days = 5 minutes_now = base.minutes_now # 720 (noon) @@ -3444,7 +3444,7 @@ def create_mock_session(*args, **kwargs): test_api.cleanup() # --- Case 2: azimuth_zero_south=False (default), azimuth=0 (North in Predbat convention) --- - # convert_azimuth(0) → 180; URL path should contain /180/ + # convert_azimuth(0) -> 180; URL path should contain /180/ test_api = create_test_solar_api() try: test_api.solar.forecast_solar = [{"latitude": 51.5, "longitude": -0.1, "declination": 30, "azimuth": 0, "kwp": 3.0, "efficiency": 1.0}] @@ -3473,8 +3473,8 @@ def test_pv_calibration_skips_system_down_days(my_predbat): drag the average downward, causing the forecast to be under-estimated. Scenario: 5 days of history. - - Days 2-5: actual = forecast = 1.0 kWh → scaling factor = 1.0 each - - Day 1 (yesterday): actual = 0.03 kWh, forecast = 1.0 kWh (3% → should be skipped) + - Days 2-5: actual = forecast = 1.0 kWh -> scaling factor = 1.0 each + - Day 1 (yesterday): actual = 0.03 kWh, forecast = 1.0 kWh (3% -> should be skipped) Expected outcome: - average_day_scaling ≈ 1.0 (only the 4 good days are used) diff --git a/apps/predbat/tests/test_solis.py b/apps/predbat/tests/test_solis.py index 7a0584ce1..50c215a86 100644 --- a/apps/predbat/tests/test_solis.py +++ b/apps/predbat/tests/test_solis.py @@ -1944,7 +1944,7 @@ async def test_write_time_windows_v2_stale_slot_clearing(): # Pass 1 should clear slot 2 charge (enable + time) even though slot 1 is also processed. # Slot 1 charge is active so Pass 1 skips it; slot 1 discharge is already clear. - # Slot 2 charge is disabled AND stale → 2 clear writes expected. + # Slot 2 charge is disabled AND stale -> 2 clear writes expected. slot2_enable_clear = next((c for c in calls if c["cid"] == SOLIS_CID_CHARGE_ENABLE_BASE + 1 and c["value"] == "0"), None) assert slot2_enable_clear is not None, "Pass 1 must clear stale slot 2 charge enable" @@ -2364,7 +2364,7 @@ async def test_write_time_windows_v1_local_time_not_utc(): outside_mode = api.set_storage_mode_calls[-1]["mode"] assert outside_mode == "Self-Use - No Timed Charge/Discharge", f"With UTC time 20:43 (outside window) expected 'Self-Use - No Timed Charge/Discharge', got '{outside_mode}'" - # The fix ensures now_utc_exact (local time) is used, so 21:43 local → Self-Use (in slot) + # The fix ensures now_utc_exact (local time) is used, so 21:43 local -> Self-Use (in slot) # If UTC were used (20:43), result would be Self-Use - No Timed Charge/Discharge (outside slot) print("PASSED: V1 mode uses local time for slot detection (regression: UTC offset no longer causes missed discharge windows)") return False @@ -3468,14 +3468,14 @@ async def mock_read_and_write_cid(sn, cid, value, field_description=None): # Test max_export_power: HA sends watts, inverter expects 100W units (÷100) api.read_and_write_cid_calls = [] entity_id = f"number.predbat_solis_{inverter_sn}_max_export_power" - await api.number_event(entity_id, 5000) # 5000 W → 50 (100W units) + await api.number_event(entity_id, 5000) # 5000 W -> 50 (100W units) assert len(api.read_and_write_cid_calls) == 1, "Should call read_and_write_cid once for max_export_power" call = api.read_and_write_cid_calls[0] assert call["cid"] == SOLIS_CID_MAX_EXPORT_POWER, f"Expected CID {SOLIS_CID_MAX_EXPORT_POWER}, got {call['cid']}" assert call["value"] == "50", f"Expected '50' (5000÷100), got {call['value']}" - # Test with a value that truncates (e.g. 550W → 5 in 100W units, not 5.5) + # Test with a value that truncates (e.g. 550W -> 5 in 100W units, not 5.5) api.read_and_write_cid_calls = [] await api.number_event(entity_id, 550) call = api.read_and_write_cid_calls[0] diff --git a/apps/predbat/tests/test_storage.py b/apps/predbat/tests/test_storage.py index 0fa958e22..a7472efab 100644 --- a/apps/predbat/tests/test_storage.py +++ b/apps/predbat/tests/test_storage.py @@ -106,7 +106,7 @@ def log(msg): # 12. age() returns None for a missing file assert run_async(storage.age("mod", "nonexistent_age")) is None, "age() should return None for missing file" - # fetch_cached: miss → calls fetch_fn once, stores, returns + # fetch_cached: miss -> calls fetch_fn once, stores, returns calls = {"n": 0} async def _fetch(): @@ -117,12 +117,12 @@ async def _fetch(): assert first == {"v": 1}, "fetch_cached miss should fetch: {}".format(first) assert calls["n"] == 1, "fetch_fn should be called exactly once on miss" - # fetch_cached: fresh hit → does NOT call fetch_fn again + # fetch_cached: fresh hit -> does NOT call fetch_fn again second = run_async(storage.fetch_cached("fc", "k", _fetch, fresh_minutes=30, stale_minutes=35, format="json")) assert second == {"v": 1}, "fresh hit should return cached value: {}".format(second) assert calls["n"] == 1, "fetch_fn must not be called on a fresh hit" - # fetch_cached: with fresh_minutes=0 every existing entry is "stale" → refresh path runs once + # fetch_cached: with fresh_minutes=0 every existing entry is "stale" -> refresh path runs once calls2 = {"n": 0} async def _fetch2(): @@ -134,13 +134,13 @@ async def _fetch2(): assert out == {"w": 1}, "stale path should refresh and return fresh data: {}".format(out) assert calls2["n"] == 1, "stale path should call fetch_fn once" - # fetch_cached: fetch_fn returning None on a hard miss → returns None, no crash + # fetch_cached: fetch_fn returning None on a hard miss -> returns None, no crash async def _fetch_none(): return None assert run_async(storage.fetch_cached("fc3", "missing", _fetch_none, format="json")) is None - # fetch_cached: stale window + fetch_fn returns None → serve cached stale value + # fetch_cached: stale window + fetch_fn returns None -> serve cached stale value run_async(storage.save("fc4", "k", {"w": 0}, format="json")) async def _fetch_none_stale(): @@ -149,7 +149,7 @@ async def _fetch_none_stale(): out = run_async(storage.fetch_cached("fc4", "k", _fetch_none_stale, fresh_minutes=0, stale_minutes=999999, format="json")) assert out == {"w": 0}, "stale path with None fetch should return cached stale: {}".format(out) - # fetch_cached: stale window + fetch_fn RAISES → serve cached stale value, do not propagate + # fetch_cached: stale window + fetch_fn RAISES -> serve cached stale value, do not propagate run_async(storage.save("fc5", "k", {"w": 7}, format="json")) async def _fetch_raise(): @@ -158,7 +158,7 @@ async def _fetch_raise(): out = run_async(storage.fetch_cached("fc5", "k", _fetch_raise, fresh_minutes=0, stale_minutes=999999, format="json")) assert out == {"w": 7}, "stale path with raising fetch should return cached stale: {}".format(out) - # fetch_cached: hard miss + fetch_fn RAISES → return None, do not propagate + # fetch_cached: hard miss + fetch_fn RAISES -> return None, do not propagate out = run_async(storage.fetch_cached("fc6", "missing", _fetch_raise, fresh_minutes=30, stale_minutes=35, format="json")) assert out is None, "hard miss with raising fetch should return None: {}".format(out) diff --git a/apps/predbat/tests/test_temperature.py b/apps/predbat/tests/test_temperature.py index 3edcc299b..2c390c494 100644 --- a/apps/predbat/tests/test_temperature.py +++ b/apps/predbat/tests/test_temperature.py @@ -1225,12 +1225,12 @@ def test_temperature(my_predbat=None): test_result = test_func(my_predbat) if test_result: failed += 1 - print(" ❌ FAILED") + print(" ERROR: FAILED") else: passed += 1 - print(" ✅ PASSED") + print(" PASS: PASSED") except Exception as e: - print(" ❌ EXCEPTION: {}".format(e)) + print(" ERROR: EXCEPTION: {}".format(e)) import traceback traceback.print_exc() failed += 1 diff --git a/apps/predbat/unit_test.py b/apps/predbat/unit_test.py index 64e68b3d9..500537591 100644 --- a/apps/predbat/unit_test.py +++ b/apps/predbat/unit_test.py @@ -15,6 +15,8 @@ import argparse from predbat import PredBat +from tests.test_auto_config import run_auto_config_tests +from tests.test_ml_load_fallback import run_ml_load_fallback_tests from tests.test_infra import TestHAInterface from tests.test_compute_metric import run_compute_metric_tests from tests.test_perf import run_perf_test @@ -210,6 +212,8 @@ def main(): # Format: (name, function, description, slow) TEST_REGISTRY = [ ("secrets", run_secrets_tests, "Secrets loading tests", False), + ("auto_config", run_auto_config_tests, "Auto Config regex tests", False), + ("ml_load_fallback", run_ml_load_fallback_tests, "ML Load Fallback tests", False), ("perf", run_perf_test, "Performance tests", False), ("model", run_model_tests, "Model tests", False), ("model_kernel", run_model_kernel_tests, "Model tests run with the C++ prediction kernel enabled", False), diff --git a/apps/predbat/userinterface.py b/apps/predbat/userinterface.py index c36fb4bb8..eaec30d34 100644 --- a/apps/predbat/userinterface.py +++ b/apps/predbat/userinterface.py @@ -1063,10 +1063,9 @@ def resolve_arg_re(self, arg, arg_value, state_keys): for item_value in arg_value: item_matched, item_value = self.resolve_arg_re(arg, item_value, state_keys) if not item_matched: - self.log("Warn: Regular argument {} expression {} failed to match - disabling this item".format(arg, item_value)) - new_list.append(None) - else: - new_list.append(item_value) + self.log("Warn: Regular argument {} expression {} failed to match - will retry".format(arg, item_value)) + matched = False + new_list.append(item_value) arg_value = new_list elif isinstance(arg_value, dict): new_dict = {} @@ -1074,10 +1073,9 @@ def resolve_arg_re(self, arg, arg_value, state_keys): item_value = arg_value[item_name] item_matched, item_value = self.resolve_arg_re(arg, item_value, state_keys) if not item_matched: - self.log("Warn: Regular argument {} expression {} failed to match - disabling this item".format(arg, item_value)) - new_dict[item_name] = None - else: - new_dict[item_name] = item_value + self.log("Warn: Regular argument {} expression {} failed to match - will retry".format(arg, item_value)) + matched = False + new_dict[item_name] = item_value arg_value = new_dict elif isinstance(arg_value, str) and arg_value.startswith("re:"): matched = False @@ -1106,10 +1104,13 @@ def auto_config(self, final=False): states = self.get_state_wrapper() state_keys = states.keys() disabled = [] - self.unmatched_args = {} + enabled = [] + + if not hasattr(self, "unmatched_args"): + self.unmatched_args = {} # Find each arg re to match - for arg in self.args: + for arg in list(self.args.keys()): arg_value = self.args[arg] matched, arg_value = self.resolve_arg_re(arg, arg_value, state_keys) if not matched: @@ -1119,7 +1120,20 @@ def auto_config(self, final=False): else: self.args[arg] = arg_value - # Remove unmatched keys + # Check previously unmatched args + for arg in list(self.unmatched_args.keys()): + arg_value = self.unmatched_args[arg] + matched, arg_value = self.resolve_arg_re(arg, arg_value, state_keys) + if matched: + self.args[arg] = arg_value + enabled.append(arg) + self.log("Info: Regular expression argument: {} matched on retry".format(arg)) + + # Remove matched keys from unmatched list + for key in enabled: + del self.unmatched_args[key] + + # Remove unmatched keys from args for key in disabled: self.unmatched_args[key] = self.args[key] del self.args[key] diff --git a/apps/predbat/utils.py b/apps/predbat/utils.py index a59088bf5..fb67123f4 100644 --- a/apps/predbat/utils.py +++ b/apps/predbat/utils.py @@ -931,6 +931,23 @@ def dp4(value): return round(value, 4) +def safe_float(v): + """ + Convert a value to a float safely, returning None for invalid values (None, NaN, Inf) + """ + import math + + if v is None: + return None + try: + v = float(v) + if math.isnan(v) or math.isinf(v): + return None + return round(v, 4) + except (ValueError, TypeError): + return None + + def minutes_to_time(updated, now): """ Compute the number of minutes between a time (now) and the updated time diff --git a/coverage/cases/predbat_debug_pre_saving1.yaml.expected.json b/coverage/cases/predbat_debug_pre_saving1.yaml.expected.json index 57b8eadf6..850b891e3 100644 --- a/coverage/cases/predbat_debug_pre_saving1.yaml.expected.json +++ b/coverage/cases/predbat_debug_pre_saving1.yaml.expected.json @@ -1 +1 @@ -{"charge_limit_best": [3.02, 0.38, 0.38, 9.52, 9.52, 9.52, 8.27, 0.38, 9.52, 9.52], "charge_window_best": [{"start": 1020, "end": 1050, "average": 25.58, "target": 3.02}, {"start": 1050, "end": 1080, "average": 25.58, "target": 0.38}, {"start": 1140, "end": 1260, "average": 25.58, "target": 0.38}, {"start": 1410, "end": 1680, "average": 7.0, "target": 9.52}, {"start": 1710, "end": 1770, "average": 7.0, "target": 9.509}, {"start": 1770, "end": 1980, "average": 25.58, "target": 9.52}, {"start": 2070, "end": 2220, "average": 25.58, "target": 8.27}, {"start": 2700, "end": 2820, "average": 25.58, "target": 0.38}, {"start": 2850, "end": 3210, "average": 7.0, "target": 9.52}, {"start": 3210, "end": 3900, "average": 25.58, "target": 9.52}], "export_window_best": [{"average": 75.0, "end": 1140, "start": 1080, "set": 69.8, "start_orig": 1080, "target": 12}, {"average": 15.0, "end": 1410, "start": 1320, "set": 14.0, "target": 4}, {"average": 15.0, "end": 1710, "start": 1680, "set": 14.0, "target": 89}], "export_limits_best": [7.0, 0, 85]} +{"charge_limit_best": [3.02, 0.38, 0.38, 0.38, 9.52, 9.52, 9.52, 0.38, 0.38, 0.38, 7.02, 0.38, 9.52, 9.52], "charge_window_best": [{"start": 1020, "end": 1050, "average": 25.58, "target": 3.02}, {"start": 1050, "end": 1080, "average": 25.58, "target": 0.38}, {"start": 1140, "end": 1200, "average": 25.58, "target": 0.38}, {"start": 1230, "end": 1380, "average": 25.58, "target": 0.38}, {"start": 1410, "end": 1660, "average": 7.0, "target": 9.478}, {"start": 1680, "end": 1735, "average": 7.0, "target": 9.519}, {"start": 1740, "end": 1770, "average": 7.0, "target": 9.52}, {"start": 1830, "end": 2100, "average": 25.58, "target": 0.38}, {"start": 2160, "end": 2250, "average": 25.58, "target": 0.38}, {"start": 2310, "end": 2340, "average": 25.58, "target": 0.38}, {"start": 2340, "end": 2370, "average": 25.58, "target": 7.02}, {"start": 2370, "end": 2430, "average": 25.58, "target": 0.38}, {"start": 2850, "end": 3210, "average": 7.0, "target": 9.52}, {"start": 3210, "end": 3900, "average": 25.58, "target": 9.52}], "export_window_best": [{"average": 75.0, "end": 1140, "start": 1080, "set": 69.8, "start_orig": 1080, "target": 12}, {"average": 15.0, "end": 1680, "start": 1660, "set": 14.0, "target": 92}, {"average": 15.0, "end": 1740, "start": 1735, "set": 14.0, "start_orig": 1710, "target": 98}], "export_limits_best": [7.0, 88.0, 94.0]} diff --git a/docs/install.md b/docs/install.md index 9db04dcac..9bca34524 100644 --- a/docs/install.md +++ b/docs/install.md @@ -270,6 +270,10 @@ mode: single Manually run the automation and then make sure the Solcast integration is working in Home Assistant by going to 'Settings' / 'Developer Tools' / 'States', filtering on 'solcast', and check that you can see the half-hourly solar forecasts in the Solcast entities. +### Solcast Clearsky Install + +If you want Predbat to model inverter clipping more accurately, you can use the [Solcast Clearsky Integration](https://github.com/autoSteve/ha-solcast-clearsky). This integration piggybacks from the base Solcast integration and automatically calculates a clearsky forecast using the Solcast array configuration, providing unclipped forecast data to Predbat. + ### No solar If you don't have any solar generation then use a file editor to comment out the following lines from the Solar forecast part of the `apps.yaml` configuration: