diff --git a/src/flameconnect/client.py b/src/flameconnect/client.py index f944d94..74e94e7 100644 --- a/src/flameconnect/client.py +++ b/src/flameconnect/client.py @@ -309,11 +309,16 @@ async def write_parameters(self, fire_id: str, params: list[Parameter]) -> None: await self._request("POST", url, json=payload) async def turn_on(self, fire_id: str) -> None: - """Turn on the fireplace, preserving current flame effect settings. + """Turn on the fireplace, preserving flame effect and heat settings. - Reads the current state first to preserve existing temperature and - flame effect configuration, then sets the mode to MANUAL and the - flame effect to ON. + Reads the current state first to preserve the existing temperature, + flame effect configuration and heat settings, then sets the mode to + MANUAL and the flame effect to ON. + + Heat settings (ParameterId 323) are written back unchanged so that + powering on cannot start or stop the heater as a side effect: a + fireplace whose heater is off stays off, and one whose heater is on + keeps its status, mode, setpoint and boost duration. Args: fire_id: The unique identifier of the fireplace. @@ -323,11 +328,14 @@ async def turn_on(self, fire_id: str) -> None: # Find current ModeParam to preserve temperature current_mode: ModeParam | None = None current_flame: FlameEffectParam | None = None + current_heat: HeatParam | None = None for param in overview.parameters: if isinstance(param, ModeParam): current_mode = param elif isinstance(param, FlameEffectParam): current_flame = param + elif isinstance(param, HeatParam): + current_heat = param temperature = ( current_mode.target_temperature @@ -343,6 +351,9 @@ async def turn_on(self, fire_id: str) -> None: new_flame = replace(current_flame, flame_effect=FlameEffect.ON) params_to_write.append(new_flame) + if current_heat is not None: + params_to_write.append(current_heat) + await self.write_parameters(fire_id, params_to_write) async def turn_off(self, fire_id: str) -> None: diff --git a/tests/test_client.py b/tests/test_client.py index d071665..85c2d6e 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -931,6 +931,99 @@ async def test_turn_on_no_flame_param_writes_only_mode(self, mock_api, token_aut assert len(body["Parameters"]) == 1 assert body["Parameters"][0]["ParameterId"] == 321 + async def test_turn_on_keeps_heat_off(self, mock_api, token_auth): + """turn_on must not start the heater when it is off. + + Powering on writes MANUAL mode; without writing HeatSettings + (323) back the heater state is left unconstrained and the fire + can start heating even though the heater was off. + """ + fire_id = "heat-off-fire" + overview_url = f"{API_BASE}/api/Fires/GetFireOverview?FireId={fire_id}" + write_url = f"{API_BASE}/api/Fires/WriteWifiParameters" + mode_val = encode_parameter( + ModeParam(mode=FireMode.STANDBY, target_temperature=20.0) + ) + heat_off_val = encode_parameter( + HeatParam( + heat_status=HeatStatus.OFF, + heat_mode=HeatMode.NORMAL, + setpoint_temperature=21.0, + boost_duration=1, + ) + ) + payload = _make_overview_payload( + fire_id=fire_id, + parameters=[ + {"ParameterId": 321, "Value": mode_val}, + {"ParameterId": 323, "Value": heat_off_val}, + ], + ) + mock_api.get(overview_url, payload=payload) + mock_api.post(write_url, payload={}) + + async with FlameConnectClient(token_auth) as client: + await client.turn_on(fire_id) + + key = ("POST", URL(write_url)) + body = mock_api.requests[key][0].kwargs["json"] + heat_wires = [p for p in body["Parameters"] if p["ParameterId"] == 323] + assert heat_wires, "turn_on did not write HeatSettings (323)" + raw = base64.b64decode(heat_wires[0]["Value"]) + # Byte 3 is heat_status: must stay 0 (OFF) + assert raw[3] == HeatStatus.OFF + + async def test_turn_on_preserves_heat_settings( + self, + mock_api, + token_auth, + get_fire_overview_payload, + ): + """turn_on writes the current heat settings back unchanged.""" + fire_id = "test-fire-001" + overview_url = f"{API_BASE}/api/Fires/GetFireOverview?FireId={fire_id}" + write_url = f"{API_BASE}/api/Fires/WriteWifiParameters" + mock_api.get(overview_url, payload=get_fire_overview_payload) + mock_api.post(write_url, payload={}) + + overview_heat = next( + p + for p in get_fire_overview_payload["WifiFireOverview"]["Parameters"] + if p["ParameterId"] == 323 + ) + + async with FlameConnectClient(token_auth) as client: + await client.turn_on(fire_id) + + key = ("POST", URL(write_url)) + body = mock_api.requests[key][0].kwargs["json"] + heat_wires = [p for p in body["Parameters"] if p["ParameterId"] == 323] + assert heat_wires, "turn_on did not write HeatSettings (323)" + assert heat_wires[0]["Value"] == overview_heat["Value"] + + async def test_turn_on_no_heat_param_writes_no_heat(self, mock_api, token_auth): + """When the fire reports no HeatParam, none is written.""" + fire_id = "no-heat-fire" + overview_url = f"{API_BASE}/api/Fires/GetFireOverview?FireId={fire_id}" + write_url = f"{API_BASE}/api/Fires/WriteWifiParameters" + mode_val = encode_parameter( + ModeParam(mode=FireMode.STANDBY, target_temperature=20.0) + ) + payload = _make_overview_payload( + fire_id=fire_id, + parameters=[{"ParameterId": 321, "Value": mode_val}], + ) + mock_api.get(overview_url, payload=payload) + mock_api.post(write_url, payload={}) + + async with FlameConnectClient(token_auth) as client: + await client.turn_on(fire_id) + + key = ("POST", URL(write_url)) + body = mock_api.requests[key][0].kwargs["json"] + param_ids = {p["ParameterId"] for p in body["Parameters"]} + assert 323 not in param_ids + # ------------------------------------------------------------------- # turn_off