Skip to content

Commit

Permalink
Introduce Entity.async_write_ha_state() to not miss state transition (#…
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
emontnemery authored and balloob committed Mar 9, 2019
1 parent 458548d commit fc81826
Show file tree
Hide file tree
Showing 3 changed files with 38 additions and 7 deletions.
6 changes: 3 additions & 3 deletions homeassistant/components/mqtt/binary_sensor.py
Expand Up @@ -117,7 +117,7 @@ async def discovery_update(self, discovery_payload):
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."""
Expand All @@ -130,7 +130,7 @@ def off_delay_listener(now):
"""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):
Expand Down Expand Up @@ -159,7 +159,7 @@ def state_message_received(_topic, payload, _qos):
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,
Expand Down
35 changes: 33 additions & 2 deletions homeassistant/helpers/entity.py
Expand Up @@ -222,6 +222,23 @@ async def async_update_ha_state(self, force_refresh=False):
_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:
Expand Down Expand Up @@ -311,13 +328,27 @@ async def async_update_ha_state(self, force_refresh=False):
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):
Expand Down
4 changes: 2 additions & 2 deletions tests/components/cast/test_media_player.py
Expand Up @@ -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'
Expand Down

0 comments on commit fc81826

Please sign in to comment.