Update Lutron light state without waiting for the controller (#181086)

This commit is contained in:
rqi14
2026-09-02 19:37:44 +02:00
committed by GitHub
parent 33cc6839f9
commit d0ebb31684
3 changed files with 97 additions and 0 deletions
+14
View File
@@ -96,6 +96,8 @@ class LutronLight(LutronDevice, LightEntity):
if ATTR_TRANSITION in kwargs:
args["fade_time_seconds"] = kwargs[ATTR_TRANSITION]
self._lutron_device.set_level(**args)
# Publish now rather than waiting for the controller to report back.
self._publish_level()
@override
def turn_off(self, **kwargs: Any) -> None:
@@ -104,6 +106,18 @@ class LutronLight(LutronDevice, LightEntity):
if ATTR_TRANSITION in kwargs:
args["fade_time_seconds"] = kwargs[ATTR_TRANSITION]
self._lutron_device.set_level(**args)
self._publish_level()
def _publish_level(self) -> None:
"""Publish the device's current level without waiting for a report.
Reads `last_level()` like `_update_callback` does, so a report that
lands around the same time cannot be overwritten by a separately held
assumed value -- whichever reached the library last is what gets
published.
"""
self._update_attrs()
self.schedule_update_ha_state()
@property
@override
+7
View File
@@ -48,6 +48,13 @@ def mock_lutron() -> Generator[MagicMock]:
light.is_dimmable = True
light.type = "LIGHT"
light.last_level.return_value = 0
# pylutron's Output.set_level() stores the new level, which last_level()
# then returns. Mirror that so the mock behaves like the library.
def _set_level(new_level, fade_time_seconds=None):
light.last_level.return_value = new_level
light.set_level.side_effect = _set_level
area.outputs.append(light)
# Mock a switch
+76
View File
@@ -226,3 +226,79 @@ async def test_light_brightness_restore(
)
# HA level 127 -> Lutron level ~49.8
light.set_level.assert_called_with(new_level=pytest.approx(50.0, abs=0.5))
async def test_light_state_published_without_waiting_for_a_report(
hass: HomeAssistant, mock_lutron: MagicMock, mock_config_entry: MockConfigEntry
) -> None:
"""State follows the command without waiting for the controller to report.
The subscription still delivers the authoritative value later, but it can
be slow, and on a HomeWorks QS system driven by keypad scenes it never
arrives for zone levels at all.
"""
mock_config_entry.add_to_hass(hass)
light = mock_lutron.areas[0].outputs[0]
light.last_level.return_value = 0
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
entity_id = "light.test_area_test_light"
assert hass.states.get(entity_id).state == STATE_OFF
# No callback is fired here: nothing reports the change back.
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 128},
blocking=True,
)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state.state == STATE_ON
assert state.attributes[ATTR_BRIGHTNESS] == 128
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_OFF
async def test_a_later_report_wins_over_the_commanded_value(
hass: HomeAssistant, mock_lutron: MagicMock, mock_config_entry: MockConfigEntry
) -> None:
"""The controller remains authoritative when it does report."""
mock_config_entry.add_to_hass(hass)
light = mock_lutron.areas[0].outputs[0]
light.last_level.return_value = 0
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
entity_id = "light.test_area_test_light"
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 128},
blocking=True,
)
await hass.async_block_till_done()
assert hass.states.get(entity_id).attributes[ATTR_BRIGHTNESS] == 128
# The controller says the zone actually landed on 100%.
light.last_level.return_value = 100
callback = light.subscribe.call_args[0][0]
callback(light, None, None, None)
await hass.async_block_till_done()
assert hass.states.get(entity_id).attributes[ATTR_BRIGHTNESS] == 255