From 4198c427362f8e6373a0b71b188c9638f7e54736 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 30 Sep 2016 21:38:39 -0700 Subject: [PATCH] Have template platforms never leave the event loop --- .../components/binary_sensor/template.py | 9 +++++--- homeassistant/components/sensor/template.py | 9 +++++--- homeassistant/components/switch/template.py | 9 +++++--- homeassistant/helpers/entity.py | 21 ++++++++++++----- .../components/binary_sensor/test_template.py | 2 +- tests/helpers/test_entity.py | 23 ++++++++++++++++++- 6 files changed, 56 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/binary_sensor/template.py b/homeassistant/components/binary_sensor/template.py index 662a6982a11..85c9f0e8950 100644 --- a/homeassistant/components/binary_sensor/template.py +++ b/homeassistant/components/binary_sensor/template.py @@ -4,6 +4,7 @@ Support for exposing a templated binary sensor. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/binary_sensor.template/ """ +import asyncio import logging import voluptuous as vol @@ -81,9 +82,10 @@ class BinarySensorTemplate(BinarySensorDevice): self.update() + @asyncio.coroutine def template_bsensor_state_listener(entity, old_state, new_state): """Called when the target device changes state.""" - self.update_ha_state(True) + yield from self.async_update_ha_state(True) track_state_change(hass, entity_ids, template_bsensor_state_listener) @@ -107,10 +109,11 @@ class BinarySensorTemplate(BinarySensorDevice): """No polling needed.""" return False - def update(self): + @asyncio.coroutine + def async_update(self): """Get the latest data and update the state.""" try: - self._state = self._template.render().lower() == 'true' + self._state = self._template.async_render().lower() == 'true' except TemplateError as ex: if ex.args and ex.args[0].startswith( "UndefinedError: 'None' has no attribute"): diff --git a/homeassistant/components/sensor/template.py b/homeassistant/components/sensor/template.py index c7c94aeaf9e..4b6f322b5aa 100644 --- a/homeassistant/components/sensor/template.py +++ b/homeassistant/components/sensor/template.py @@ -4,6 +4,7 @@ Allows the creation of a sensor that breaks out state_attributes. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.template/ """ +import asyncio import logging import voluptuous as vol @@ -78,9 +79,10 @@ class SensorTemplate(Entity): self.update() + @asyncio.coroutine def template_sensor_state_listener(entity, old_state, new_state): """Called when the target device changes state.""" - self.update_ha_state(True) + yield from self.async_update_ha_state(True) track_state_change(hass, entity_ids, template_sensor_state_listener) @@ -104,10 +106,11 @@ class SensorTemplate(Entity): """No polling needed.""" return False - def update(self): + @asyncio.coroutine + def async_update(self): """Get the latest data and update the states.""" try: - self._state = self._template.render() + self._state = self._template.async_render() except TemplateError as ex: if ex.args and ex.args[0].startswith( "UndefinedError: 'None' has no attribute"): diff --git a/homeassistant/components/switch/template.py b/homeassistant/components/switch/template.py index 5358a23d8c6..7c6f4f5886d 100644 --- a/homeassistant/components/switch/template.py +++ b/homeassistant/components/switch/template.py @@ -4,6 +4,7 @@ Support for switches which integrates with other components. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/switch.template/ """ +import asyncio import logging import voluptuous as vol @@ -87,9 +88,10 @@ class SwitchTemplate(SwitchDevice): self.update() + @asyncio.coroutine def template_switch_state_listener(entity, old_state, new_state): """Called when the target device changes state.""" - self.update_ha_state(True) + yield from self.async_update_ha_state(True) track_state_change(hass, entity_ids, template_switch_state_listener) @@ -121,10 +123,11 @@ class SwitchTemplate(SwitchDevice): """Fire the off action.""" self._off_script.run() - def update(self): + @asyncio.coroutine + def async_update(self): """Update the state from the template.""" try: - state = self._template.render().lower() + state = self._template.async_render().lower() if state in _VALID_STATES: self._state = state in ('true', STATE_ON) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 7529d6288ab..3c119eb456e 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -49,6 +49,11 @@ class Entity(object): # SAFE TO OVERWRITE # The properties and methods here are safe to overwrite when inheriting # this class. These may be used to customize the behavior of the entity. + entity_id = None # type: str + + # Owning hass instance. Will be set by EntityComponent + hass = None # type: Optional[HomeAssistant] + @property def should_poll(self) -> bool: """Return True if entity has to be polled for state. @@ -128,18 +133,22 @@ class Entity(object): return False def update(self): - """Retrieve latest state.""" - pass + """Retrieve latest state. - entity_id = None # type: str + When not implemented, will forward call to async version if available. + """ + async_update = getattr(self, 'async_update', None) + + if async_update is None: + return + + run_coroutine_threadsafe(async_update(), self.hass.loop).result() # DO NOT OVERWRITE # These properties and methods are either managed by Home Assistant or they # are used to perform a very specific function. Overwriting these may # produce undesirable effects in the entity's operation. - hass = None # type: Optional[HomeAssistant] - def update_ha_state(self, force_refresh=False): """Update Home Assistant with current state of entity. @@ -172,7 +181,7 @@ class Entity(object): if force_refresh: if hasattr(self, 'async_update'): # pylint: disable=no-member - self.async_update() + yield from self.async_update() else: # PS: Run this in our own thread pool once we have # future support? diff --git a/tests/components/binary_sensor/test_template.py b/tests/components/binary_sensor/test_template.py index 7337bd4de03..c9e4bf6138b 100644 --- a/tests/components/binary_sensor/test_template.py +++ b/tests/components/binary_sensor/test_template.py @@ -119,7 +119,7 @@ class TestBinarySensorTemplate(unittest.TestCase): vs.update_ha_state() self.hass.block_till_done() - with mock.patch.object(vs, 'update') as mock_update: + with mock.patch.object(vs, 'async_update') as mock_update: self.hass.bus.fire(EVENT_STATE_CHANGED) self.hass.block_till_done() assert mock_update.call_count == 1 diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 81ef17ff0fd..f63e80ec1f9 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -53,7 +53,12 @@ def test_async_update_support(event_loop): assert len(sync_update) == 1 assert len(async_update) == 0 - ent.async_update = lambda: async_update.append(1) + @asyncio.coroutine + def async_update_func(): + """Async update.""" + async_update.append(1) + + ent.async_update = async_update_func event_loop.run_until_complete(test()) @@ -95,3 +100,19 @@ class TestHelpersEntity(object): assert entity.generate_entity_id( fmt, 'overwrite hidden true', hass=self.hass) == 'test.overwrite_hidden_true_2' + + def test_update_calls_async_update_if_available(self): + """Test async update getting called.""" + async_update = [] + + class AsyncEntity(entity.Entity): + hass = self.hass + entity_id = 'sensor.test' + + @asyncio.coroutine + def async_update(self): + async_update.append([1]) + + ent = AsyncEntity() + ent.update() + assert len(async_update) == 1