From e740556d9a1871026d00d429bedb8d372b480d8d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 25 May 2026 12:21:06 +0000 Subject: [PATCH 1/2] fix: guard against None in data_collection projected_temperature round() Agent-Logs-Url: https://github.com/Gamso/smart_fan_controller/sessions/87b4ec4e-2fc0-4309-b6b8-ae03ea4b43b0 Co-authored-by: Gamso <13354981+Gamso@users.noreply.github.com> --- .../smart_fan_controller/data_collection.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/custom_components/smart_fan_controller/data_collection.py b/custom_components/smart_fan_controller/data_collection.py index 52662bf..1b2cc2a 100644 --- a/custom_components/smart_fan_controller/data_collection.py +++ b/custom_components/smart_fan_controller/data_collection.py @@ -50,6 +50,12 @@ _MAX_FILE_SIZE = 10 * 1024 * 1024 +def _coalesce(value, default): + """Return *value* if it is not None, otherwise *default*.""" + return value if value is not None else default + + + class DataCollector: """Appends one CSV row per control cycle to a rotating log file.""" @@ -91,8 +97,8 @@ async def async_record( round(decision.get("temperature_error", 0.0), 3), round(vtherm_slope, 4), round(effective_slope, 4), - round(decision.get("projected_temperature", current_temp), 3), - round(decision.get("projected_temperature_error", 0.0), 3), + round(_coalesce(decision.get("projected_temperature"), current_temp), 3), + round(_coalesce(decision.get("projected_temperature_error"), 0.0), 3), phase, round(decision.get("minutes_since_last_change", 0.0), 2), round(effective_timeout, 2), From aaf038a1f733a4b08133fe8d8b0351ef25d4b45a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 28 May 2026 20:58:44 +0000 Subject: [PATCH 2/2] Add regression test for None projected values --- tests/test_data_collection.py | 38 +++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/test_data_collection.py b/tests/test_data_collection.py index 8f9bff0..e6660bf 100644 --- a/tests/test_data_collection.py +++ b/tests/test_data_collection.py @@ -98,6 +98,44 @@ async def test_async_record_appends_row(tmp_path: Path) -> None: assert hass.async_add_executor_job.await_count == 2 +@pytest.mark.asyncio +async def test_async_record_handles_none_projected_values(tmp_path: Path) -> None: + """None projected values should fallback without raising.""" + hass = _make_executor_hass() + collector = DataCollector(hass, str(tmp_path), "123456789") + + await collector.async_initialize() + await collector.async_record( + hvac_mode="heat", + current_temp=20.1234, + target_temp=21.0, + vtherm_slope=-0.2468, + is_window_open=False, + decision={ + "temperature_error": 0.8765, + "projected_temperature": None, + "projected_temperature_error": None, + "minutes_since_last_change": 12.345, + "current_fan": "low", + "fan_mode": "medium", + "reason": "Strong recovery", + }, + phase="TRANSIENT", + effective_slope=-0.2468, + effective_timeout=15.4321, + force=True, + learning_ready=False, + dead_time=10.987, + ) + + with open(collector.path, newline="", encoding="utf-8") as fh: + rows = list(csv.reader(fh)) + + assert len(rows) == 2 + assert rows[1][7:9] == ["20.123", "0.0"] + assert hass.async_add_executor_job.await_count == 2 + + @pytest.mark.asyncio async def test_async_initialize_rotates_file_on_header_change(tmp_path: Path) -> None: """The collector should rotate the active CSV when the schema changes."""