From fc818267634aed998bba7733de4eb0021669167e Mon Sep 17 00:00:00 2001 From: emontnemery Date: Sat, 9 Mar 2019 18:52:22 +0100 Subject: [PATCH] Introduce Entity.async_write_ha_state() to not miss state transition (#21590) * Copy state in schedule_update_ha_state * Lint * Fix broken test * Review comment, improve docstring * Preserve order of state updates * Rewrite * Break up async_update_ha_state * Update binary_sensor.py * Review comments * Update docstring * hass -> ha * Update entity.py * Update entity.py --- .../components/mqtt/binary_sensor.py | 6 ++-- homeassistant/helpers/entity.py | 35 +++++++++++++++++-- tests/components/cast/test_media_player.py | 4 +-- 3 files changed, 38 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index cb93712776cb..103958376c00 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -117,7 +117,7 @@ class MqttBinarySensor(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, await self.availability_discovery_update(config) await self.device_info_discovery_update(config) await self._subscribe_topics() - self.async_schedule_update_ha_state() + self.async_write_ha_state() async def _subscribe_topics(self): """(Re)Subscribe to topics.""" @@ -130,7 +130,7 @@ class MqttBinarySensor(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, """Switch device off after a delay.""" self._delay_listener = None self._state = False - self.async_schedule_update_ha_state() + self.async_write_ha_state() @callback def state_message_received(_topic, payload, _qos): @@ -159,7 +159,7 @@ class MqttBinarySensor(MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, self._delay_listener = evt.async_call_later( self.hass, off_delay, off_delay_listener) - self.async_schedule_update_ha_state() + self.async_write_ha_state() self._sub_state = await subscription.async_subscribe_topics( self.hass, self._sub_state, diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index dd9677f65155..4ef5513baf7b 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -222,6 +222,23 @@ class Entity: _LOGGER.exception("Update for %s fails", self.entity_id) return + self._async_write_ha_state() + + @callback + def async_write_ha_state(self): + """Write the state to the state machine.""" + if self.hass is None: + raise RuntimeError("Attribute hass is None for {}".format(self)) + + if self.entity_id is None: + raise NoEntitySpecifiedError( + "No entity id specified for entity {}".format(self.name)) + + self._async_write_ha_state() + + @callback + def _async_write_ha_state(self): + """Write the state to the state machine.""" start = timer() if not self.available: @@ -311,13 +328,27 @@ class Entity: def schedule_update_ha_state(self, force_refresh=False): """Schedule an update ha state change task. - That avoid executor dead looks. + Scheduling the update avoids executor deadlocks. + + Entity state and attributes are read when the update ha state change + task is executed. + If state is changed more than once before the ha state change task has + been executed, the intermediate state transitions will be missed. """ self.hass.add_job(self.async_update_ha_state(force_refresh)) @callback def async_schedule_update_ha_state(self, force_refresh=False): - """Schedule an update ha state change task.""" + """Schedule an update ha state change task. + + This method must be run in the event loop. + Scheduling the update avoids executor deadlocks. + + Entity state and attributes are read when the update ha state change + task is executed. + If state is changed more than once before the ha state change task has + been executed, the intermediate state transitions will be missed. + """ self.hass.async_create_task(self.async_update_ha_state(force_refresh)) async def async_device_update(self, warning=True): diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index b5d6220904ff..66a975a226e6 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -275,16 +275,16 @@ async def test_entity_media_states(hass: HomeAssistantType): state = hass.states.get('media_player.speaker') assert state.state == 'playing' - entity.new_media_status(media_status) media_status.player_is_playing = False media_status.player_is_paused = True + entity.new_media_status(media_status) await hass.async_block_till_done() state = hass.states.get('media_player.speaker') assert state.state == 'paused' - entity.new_media_status(media_status) media_status.player_is_paused = False media_status.player_is_idle = True + entity.new_media_status(media_status) await hass.async_block_till_done() state = hass.states.get('media_player.speaker') assert state.state == 'idle'