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
19 changes: 15 additions & 4 deletions src/flameconnect/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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:
Expand Down
93 changes: 93 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down