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: 17 additions & 2 deletions custom_components/myhome/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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:
Expand Down
54 changes: 54 additions & 0 deletions tests/test_component_light.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Loading