diff --git a/homeassistant/components/homeassistant/__init__.py b/homeassistant/components/homeassistant/__init__.py index f1b8d4e87d6..c2ee40b7d43 100644 --- a/homeassistant/components/homeassistant/__init__.py +++ b/homeassistant/components/homeassistant/__init__.py @@ -21,7 +21,7 @@ from homeassistant.const import ( import homeassistant.core as ha from homeassistant.exceptions import HomeAssistantError, Unauthorized, UnknownUser from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.service import async_extract_entity_ids +from homeassistant.helpers.service import async_extract_referenced_entity_ids _LOGGER = logging.getLogger(__name__) DOMAIN = ha.DOMAIN @@ -37,39 +37,37 @@ async def async_setup(hass: ha.HomeAssistant, config: dict) -> bool: async def async_handle_turn_service(service): """Handle calls to homeassistant.turn_on/off.""" - entity_ids = await async_extract_entity_ids(hass, service) + referenced = await async_extract_referenced_entity_ids(hass, service) + all_referenced = referenced.referenced | referenced.indirectly_referenced # Generic turn on/off method requires entity id - if not entity_ids: + if not all_referenced: _LOGGER.error( - "homeassistant/%s cannot be called without entity_id", service.service + "homeassistant.%s cannot be called without a target", service.service ) return # Group entity_ids by domain. groupby requires sorted data. by_domain = it.groupby( - sorted(entity_ids), lambda item: ha.split_entity_id(item)[0] + sorted(all_referenced), lambda item: ha.split_entity_id(item)[0] ) tasks = [] + unsupported_entities = set() for domain, ent_ids in by_domain: # This leads to endless loop. if domain == DOMAIN: _LOGGER.warning( - "Called service homeassistant.%s with invalid entity IDs %s", + "Called service homeassistant.%s with invalid entities %s", service.service, ", ".join(ent_ids), ) continue - # We want to block for all calls and only return when all calls - # have been processed. If a service does not exist it causes a 10 - # second delay while we're blocking waiting for a response. - # But services can be registered on other HA instances that are - # listening to the bus too. So as an in between solution, we'll - # block only if the service is defined in the current HA instance. - blocking = hass.services.has_service(domain, service.service) + if not hass.services.has_service(domain, service.service): + unsupported_entities.update(set(ent_ids) & referenced.referenced) + continue # Create a new dict for this call data = dict(service.data) @@ -79,10 +77,21 @@ async def async_setup(hass: ha.HomeAssistant, config: dict) -> bool: tasks.append( hass.services.async_call( - domain, service.service, data, blocking, context=service.context + domain, + service.service, + data, + blocking=True, + context=service.context, ) ) + if unsupported_entities: + _LOGGER.warning( + "The service homeassistant.%s does not support entities %s", + service.service, + ", ".join(sorted(unsupported_entities)), + ) + if tasks: await asyncio.gather(*tasks) diff --git a/tests/components/homeassistant/test_init.py b/tests/components/homeassistant/test_init.py index 3ad3ef76483..ca2f116a06a 100644 --- a/tests/components/homeassistant/test_init.py +++ b/tests/components/homeassistant/test_init.py @@ -248,7 +248,7 @@ class TestComponentsCore(unittest.TestCase): assert not mock_stop.called -async def test_turn_on_to_not_block_for_domains_without_service(hass): +async def test_turn_on_skips_domains_without_service(hass, caplog): """Test if turn_on is blocking domain with no service.""" await async_setup_component(hass, "homeassistant", {}) async_mock_service(hass, "light", SERVICE_TURN_ON) @@ -261,7 +261,7 @@ async def test_turn_on_to_not_block_for_domains_without_service(hass): service_call = ha.ServiceCall( "homeassistant", "turn_on", - {"entity_id": ["light.test", "sensor.bla", "light.bla"]}, + {"entity_id": ["light.test", "sensor.bla", "binary_sensor.blub", "light.bla"]}, ) service = hass.services._services["homeassistant"]["turn_on"] @@ -271,18 +271,19 @@ async def test_turn_on_to_not_block_for_domains_without_service(hass): ) as mock_call: await service.job.target(service_call) - assert mock_call.call_count == 2 + assert mock_call.call_count == 1 assert mock_call.call_args_list[0][0] == ( "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, + assert mock_call.call_args_list[0][1] == { + "blocking": True, + "context": service_call.context, + } + assert ( + "The service homeassistant.turn_on does not support entities binary_sensor.blub, sensor.bla" + in caplog.text ) @@ -381,6 +382,6 @@ async def test_not_allowing_recursion(hass, caplog): blocking=True, ) assert ( - f"Called service homeassistant.{service} with invalid entity IDs homeassistant.light" + f"Called service homeassistant.{service} with invalid entities homeassistant.light" in caplog.text ), service