fix(climate): write mode-correct setpoints and stop REST reverting live state
Validate / HACS validation (pull_request) Skipped
Validate / Hassfest validation (pull_request) Skipped

Two independent bugs made a second change (e.g. adjusting the target
temperature right after switching cool -> heat) fail to take, and made
Home Assistant settle back on a previous state while DKN Cloud NA showed
the correct one.

Wrong setpoint key on write:

_writable_temperature_property_for_mode() ignored its hvac_mode argument
and resolved the key from real_mode/mode in the cached device payload.
That payload still describes the pre-change state, so a heat setpoint was
emitted as setpoint_air_cool: the unit switched to heat but the heat
setpoint never moved. The same defect broke AUTO in steady state, where
real_mode=cool made the write target setpoint_air_cool while the read
used setpoint_air_auto, so the value could never appear to change.

target_temperature() and writable_target_temperature_key() now accept the
mode the caller intends, and that mode takes priority over anything in the
device payload. Device-reported modes remain a fallback only for payloads
that do not expose the requested mode's setpoint.

async_set_temperature() no longer re-sends power+mode; async_set_hvac_mode()
already sent them, and the stale-cache condition guarding the resend was
always true. It now waits (bounded) for the cloud to echo the mode before
sending the setpoint, matching the ensure_mode ordering the smoke test uses.

REST snapshot reverting live state:

The coordinator merged {**existing, **device}, letting the lagging REST
/installations snapshot win over live Socket.IO pushes. Once the socket
confirmed a write, _reconcile_optimistic dropped the overlay, leaving
nothing to stop the next poll from reverting the state.

Socket-pushed values are now tracked per device and applied after the REST
payload. They are discarded when the socket session changes, since updates
may have been missed while it was down and REST becomes authoritative again.

Also report hvac_action and target_temperature against the effective
(optimistic-aware) mode so the action cannot contradict the reported mode
while a change propagates.

Co-authored-by: anthropic/claude-opus-5
This commit is contained in:
2026-09-19 09:48:10 -03:00
co-authored by anthropic/claude-opus-5
parent 41a05a1a69
commit 3929546ea5
7 changed files with 145 additions and 42 deletions
+41 -12
View File
@@ -84,19 +84,35 @@ def target_temperature_key(mode: int | None) -> str | None:
return None
def target_temperature(data: dict[str, Any]) -> float | None:
"""Return the requested target temperature in Celsius."""
key = target_temperature_key(requested_mode(data))
def target_temperature(data: dict[str, Any], mode: int | None = None) -> float | None:
"""Return the target temperature in Celsius for ``mode``.
``mode`` defaults to the device's requested mode. Callers that already know
the effective mode (for example while an optimistic mode change is still
propagating) should pass it so the read uses the same setpoint key the
write used.
"""
key = target_temperature_key(mode if mode is not None else requested_mode(data))
if key is None:
return None
return to_celsius(data.get(key), data.get("units"))
def writable_target_temperature_key(data: dict[str, Any]) -> str | None:
"""Return the setpoint key most likely to be writable for the current state."""
def writable_target_temperature_key(
data: dict[str, Any], mode: int | None = None
) -> str | None:
"""Return the setpoint key to write for ``mode``.
``mode`` is the mode the write is intended for and takes priority over
anything in ``data``, which may still describe the pre-change state while a
mode change propagates through the cloud. The device-reported modes are
only consulted as a fallback for payloads that do not expose the requested
mode's setpoint at all.
"""
candidates: list[str] = []
for mode in (live_mode(data), requested_mode(data), DEVICE_MODE_AUTO):
key = target_temperature_key(mode)
preference = (mode, live_mode(data), requested_mode(data), DEVICE_MODE_AUTO)
for candidate_mode in preference:
key = target_temperature_key(candidate_mode)
if key is not None and key not in candidates:
candidates.append(key)
@@ -107,15 +123,28 @@ def writable_target_temperature_key(data: dict[str, Any]) -> str | None:
return candidates[0] if candidates else None
def inferred_hvac_action(data: dict[str, Any]) -> str:
"""Infer the active HVAC action from requested mode and temperatures."""
power = as_bool(data.get("power"))
def inferred_hvac_action(
data: dict[str, Any],
mode: int | None = None,
target: float | None = None,
power: bool | None = None,
) -> str:
"""Infer the active HVAC action from the effective mode and temperatures.
``mode``, ``target`` and ``power`` let callers supply the effective
(possibly still optimistic) values so the reported action matches the
reported mode.
"""
if power is None:
power = as_bool(data.get("power"))
if not power:
return "off"
mode = requested_mode(data)
if mode is None:
mode = requested_mode(data)
current = current_temperature(data)
target = target_temperature(data)
if target is None:
target = target_temperature(data, mode)
if mode == DEVICE_MODE_HEAT:
if current is not None and target is not None and current < target: