diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index e2701ee37f1..bdb89dd60fa 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -12,6 +12,8 @@ import itertools as it import logging from typing import Awaitable +import voluptuous as vol + import homeassistant.core as ha import homeassistant.config as conf_util from homeassistant.exceptions import HomeAssistantError @@ -21,11 +23,16 @@ from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, SERVICE_HOMEASSISTANT_STOP, SERVICE_HOMEASSISTANT_RESTART, RESTART_EXIT_CODE) +from homeassistant.helpers import config_validation as cv _LOGGER = logging.getLogger(__name__) SERVICE_RELOAD_CORE_CONFIG = 'reload_core_config' SERVICE_CHECK_CONFIG = 'check_config' +SERVICE_UPDATE_ENTITY = 'update_entity' +SCHEMA_UPDATE_ENTITY = vol.Schema({ + ATTR_ENTITY_ID: cv.entity_id +}) def is_on(hass, entity_id=None): @@ -133,12 +140,20 @@ async def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]: if call.service == SERVICE_HOMEASSISTANT_RESTART: hass.async_create_task(hass.async_stop(RESTART_EXIT_CODE)) + async def async_handle_update_service(call): + """Service handler for updating an entity.""" + await hass.helpers.entity_component.async_update_entity( + call.data[ATTR_ENTITY_ID]) + hass.services.async_register( ha.DOMAIN, SERVICE_HOMEASSISTANT_STOP, async_handle_core_service) hass.services.async_register( ha.DOMAIN, SERVICE_HOMEASSISTANT_RESTART, async_handle_core_service) hass.services.async_register( ha.DOMAIN, SERVICE_CHECK_CONFIG, async_handle_core_service) + hass.services.async_register( + ha.DOMAIN, SERVICE_UPDATE_ENTITY, async_handle_update_service, + schema=SCHEMA_UPDATE_ENTITY) async def async_handle_reload_config(call): """Service handler for reloading core config.""" diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 60fd661a765..987bdeae6ca 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -346,7 +346,7 @@ class Entity: if hasattr(self, 'async_update'): await self.async_update() elif hasattr(self, 'update'): - await self.hass.async_add_job(self.update) + await self.hass.async_add_executor_job(self.update) finally: self._update_staged = False if warning: diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index c2ab8722c97..982c92510a9 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -2,6 +2,7 @@ import asyncio from datetime import timedelta from itertools import chain +import logging from homeassistant import config as conf_util from homeassistant.setup import async_prepare_setup_platform @@ -11,10 +12,33 @@ from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform, discovery from homeassistant.helpers.service import extract_entity_ids +from homeassistant.loader import bind_hass from homeassistant.util import slugify from .entity_platform import EntityPlatform DEFAULT_SCAN_INTERVAL = timedelta(seconds=15) +DATA_INSTANCES = 'entity_components' + + +@bind_hass +async def async_update_entity(hass, entity_id): + """Trigger an update for an entity.""" + domain = entity_id.split('.', 1)[0] + entity_comp = hass.data.get(DATA_INSTANCES, {}).get(domain) + + if entity_comp is None: + logging.getLogger(__name__).warning( + 'Forced update failed. Component for %s not loaded.', entity_id) + return + + entity = entity_comp.get_entity(entity_id) + + if entity is None: + logging.getLogger(__name__).warning( + 'Forced update failed. Entity %s not found.', entity_id) + return + + await entity.async_update_ha_state(True) class EntityComponent: @@ -45,6 +69,8 @@ class EntityComponent: self.async_add_entities = self._platforms[domain].async_add_entities self.add_entities = self._platforms[domain].add_entities + hass.data.setdefault(DATA_INSTANCES, {})[domain] = self + @property def entities(self): """Return an iterable that returns all entities.""" diff --git a/tests/components/test_init.py b/tests/components/test_init.py index 68396f5abcb..b9152bbdd6a 100644 --- a/tests/components/test_init.py +++ b/tests/components/test_init.py @@ -355,3 +355,17 @@ async def test_turn_on_to_not_block_for_domains_without_service(hass): 'light', 'turn_on', {'entity_id': ['light.bla', 'light.test']}, True) assert mock_call.call_args_list[1][0] == ( 'sensor', 'turn_on', {'entity_id': ['sensor.bla']}, False) + + +async def test_entity_update(hass): + """Test being able to call entity update.""" + await comps.async_setup(hass, {}) + + with patch('homeassistant.helpers.entity_component.async_update_entity', + return_value=mock_coro()) as mock_update: + await hass.services.async_call('homeassistant', 'update_entity', { + 'entity_id': 'light.kitchen' + }, blocking=True) + + assert len(mock_update.mock_calls) == 1 + assert mock_update.mock_calls[0][1][1] == 'light.kitchen' diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 1a0c248383b..c853d0b3447 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -415,3 +415,19 @@ async def test_unload_entry_fails_if_never_loaded(hass): with pytest.raises(ValueError): await component.async_unload_entry(entry) + + +async def test_update_entity(hass): + """Test that we can update an entity with the helper.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + entity = MockEntity() + entity.async_update_ha_state = Mock(return_value=mock_coro()) + await component.async_add_entities([entity]) + + # Called as part of async_add_entities + assert len(entity.async_update_ha_state.mock_calls) == 1 + + await hass.helpers.entity_component.async_update_entity(entity.entity_id) + + assert len(entity.async_update_ha_state.mock_calls) == 2 + assert entity.async_update_ha_state.mock_calls[-1][1][0] is True