Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions custom_components/smart_fan_controller/data_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down Expand Up @@ -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),
Comment thread
Gamso marked this conversation as resolved.
phase,
round(decision.get("minutes_since_last_change", 0.0), 2),
round(effective_timeout, 2),
Expand Down
38 changes: 38 additions & 0 deletions tests/test_data_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
Loading