From a294323d092563001c236f7202a26df5447ef9a2 Mon Sep 17 00:00:00 2001 From: Laurens <43173895+GreenGrassBlueOcean@users.noreply.github.com> Date: Thu, 24 Sep 2026 19:26:19 +0200 Subject: [PATCH] fix(light): an unknown WHAT does not keep a state restored at startup Live on the MH200 with the #456 test build: light.light_74 came back "on" from the state the WHAT 19 bug had left, and since the bus only ever answers *1*19*74## (+ a WHO 1001 fault mask), "keep the last state" kept that stale "on" forever. A state seen on the bus or set from Home Assistant is still kept against an unknown WHAT; one that was only restored at startup now becomes unknown on the first fault report, and the warning says so. Co-Authored-By: Claude Opus 5.5 --- custom_components/myhome/light.py | 19 +++++++++-- tests/test_component_light.py | 54 +++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/custom_components/myhome/light.py b/custom_components/myhome/light.py index 5697e91f..42dbf48b 100644 --- a/custom_components/myhome/light.py +++ b/custom_components/myhome/light.py @@ -372,6 +372,9 @@ def __init__( self._attr_icon = self._off_icon self._attr_is_on = None + # True while is_on is only what Home Assistant restored at startup: not + # worth keeping against a fault report (an actuator stuck at WHAT 19). + self._is_on_restored = False self._attr_brightness: int | None = None self._attr_brightness_pct: int | None = None @@ -471,8 +474,10 @@ async def async_restore_last_state(self, last_state: State) -> None: # 5. Restore power state if last_state.state == "on": self._attr_is_on = True + self._is_on_restored = True elif last_state.state == "off": self._attr_is_on = False + self._is_on_restored = True async def async_update(self) -> None: """Update the entity. @@ -533,6 +538,7 @@ def _apply_brightness_state(self, pct: int, is_on: bool | None = None) -> None: self._attr_is_on = is_on else: self._attr_is_on = pct > 0 + self._is_on_restored = False if pct > 0: self._last_brightness_pct = pct @@ -665,6 +671,7 @@ async def async_turn_on_timed( ) await self._gateway_handler.send(cmd) self._attr_is_on = True + self._is_on_restored = False self.async_write_ha_state() async def async_turn_on(self, **kwargs: Any) -> None: @@ -752,6 +759,7 @@ async def async_turn_on(self, **kwargs: Any) -> None: if ATTR_BRIGHTNESS not in kwargs and ATTR_BRIGHTNESS_PCT not in kwargs: self._attr_is_on = True + self._is_on_restored = False if self._attr_brightness is None and self._last_brightness_pct: self._apply_brightness_state(self._last_brightness_pct, is_on=True) self.async_schedule_update_ha_state() @@ -871,17 +879,24 @@ def handle_event(self, message: OWNLightingEvent) -> None: ) if message.is_on is not None: self._attr_is_on = message.is_on + self._is_on_restored = False # A WHAT outside the WHO 1 table (e.g. 19 from an MH200 actuator with a - # WHO 1001 fault) leaves is_on None: keep the last state, show the value. + # WHO 1001 fault) leaves is_on None: keep a state seen on the bus or set + # from Home Assistant, but not one restored at startup - that may be the + # "on" a fault left behind before OWNd knew better (light 74, #456). unknown_state = getattr(message, "unknown_state", None) if isinstance(unknown_state, int): + if self._is_on_restored: + self._attr_is_on = None + self._is_on_restored = False if self._attr_extra_state_attributes.get("unknown_state") != unknown_state: LOGGER.warning( - "%s light %s reports unknown lighting WHAT %s; keeping its last state", + "%s light %s reports unknown lighting WHAT %s; %s", self._gateway_handler.log_id, self._full_where, unknown_state, + "its state is unknown" if self._attr_is_on is None else "keeping its last state", ) self._attr_extra_state_attributes["unknown_state"] = unknown_state elif message.is_on is not None: diff --git a/tests/test_component_light.py b/tests/test_component_light.py index 89b3d71e..cfa7d793 100644 --- a/tests/test_component_light.py +++ b/tests/test_component_light.py @@ -15,6 +15,7 @@ LightEntityFeature, ) from homeassistant.const import CONF_NAME +from homeassistant.core import State from homeassistant.helpers.dispatcher import async_dispatcher_send from OWNd.message import ( OWNEvent, @@ -1841,3 +1842,56 @@ def test_mh200_what_19_reply_does_not_turn_the_light_on(hass): assert light.is_on is False assert light.extra_state_attributes["unknown_state"] == 19 + + +def _fault_event(value=19): + event = MagicMock(spec=OWNLightingEvent, is_on=None, brightness=None, brightness_preset=None) + event.unknown_state = value + return event + + +async def test_unknown_state_does_not_keep_a_restored_state(hass, caplog): + """A restored "on" may be the one the fault left behind: the next fault report makes it unknown.""" + light = _unknown_state_light(hass) + await light.async_restore_last_state(State("light.light_74", "on")) + assert light.is_on is True + + with caplog.at_level(logging.WARNING, logger="custom_components.myhome"): + light.handle_event(_fault_event()) + assert light.is_on is None + assert light.state is None # Home Assistant shows "unknown" + assert light.extra_state_attributes["unknown_state"] == 19 + assert any("its state is unknown" in r.getMessage() for r in caplog.records) + + # the next real state from the bus is taken, and then kept against the fault + light.handle_event(MagicMock(spec=OWNLightingEvent, is_on=False, brightness=None, brightness_preset=None)) + light.handle_event(_fault_event()) + assert light.is_on is False + + +async def test_unknown_state_keeps_a_state_set_from_home_assistant(hass): + light = _unknown_state_light(hass) + light.entity_id = "light.light_74" + light.async_write_ha_state = MagicMock() + light._gateway_handler.send = AsyncMock() + await light.async_restore_last_state(State("light.light_74", "off")) + await light.async_turn_on_timed(duration=60) + light.handle_event(_fault_event()) + assert light.is_on is True + + +@pytest.mark.xfail( + _OWND_WHAT_19_IS_ON, + reason="installed OWNd reports lighting WHAT 19 (outside the WHO 1 table) as on", + strict=True, +) +async def test_mh200_light_74_restored_on_after_the_fix(hass): + """Live 2026-09-24 17:16 on the MH200 test build: light.light_74 came back "on" from the + state the bug had left, and the bus only ever answered *1*19*74## (+ WHO 1001 DIMENSION 11).""" + light = _unknown_state_light(hass) + await light.async_restore_last_state(State("light.light_74", "on")) + + light.handle_event(OWNEvent.parse("*1*19*74##")) + + assert light.is_on is None + assert light.extra_state_attributes["unknown_state"] == 19