From f41623ca6423ac2e83b10801aac4d062f9f9b72f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 4 Feb 2020 14:42:07 -0800 Subject: [PATCH] Log warning when entities referenced in service call not found (#31427) * Raise entities not found error * Make it a warning, not an error * Add support for MATCH_ENTITY_NONE * Fix lint * Fix tests --- homeassistant/components/amcrest/__init__.py | 4 + .../components/denonavr/media_player.py | 5 + homeassistant/components/insteon/schemas.py | 5 +- homeassistant/components/lifx/light.py | 4 + homeassistant/components/tts/__init__.py | 6 +- .../components/webostv/media_player.py | 4 + homeassistant/const.py | 1 + homeassistant/helpers/config_validation.py | 9 +- homeassistant/helpers/service.py | 54 ++++++-- tests/components/google_translate/test_tts.py | 24 +++- tests/components/marytts/test_tts.py | 21 ++- tests/components/tts/test_init.py | 122 +++++++++++------- tests/components/voicerss/test_tts.py | 26 +++- tests/components/yandextts/test_tts.py | 39 ++++-- tests/helpers/test_entity_component.py | 72 ++++++++++- tests/helpers/test_service.py | 32 ++++- 16 files changed, 339 insertions(+), 89 deletions(-) diff --git a/homeassistant/components/amcrest/__init__.py b/homeassistant/components/amcrest/__init__.py index f7814939e3a..d1e1aafa6f3 100644 --- a/homeassistant/components/amcrest/__init__.py +++ b/homeassistant/components/amcrest/__init__.py @@ -23,6 +23,7 @@ from homeassistant.const import ( CONF_SENSORS, CONF_USERNAME, ENTITY_MATCH_ALL, + ENTITY_MATCH_NONE, HTTP_BASIC_AUTHENTICATION, ) from homeassistant.exceptions import Unauthorized, UnknownUser @@ -236,6 +237,9 @@ def setup(hass, config): if have_permission(user, entity_id) ] + if call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_NONE: + return [] + call_ids = await async_extract_entity_ids(hass, call) entity_ids = [] for entity_id in hass.data[DATA_AMCREST][CAMERAS]: diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index 350d065f9d9..b14592d1b78 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -30,6 +30,7 @@ from homeassistant.const import ( CONF_TIMEOUT, CONF_ZONE, ENTITY_MATCH_ALL, + ENTITY_MATCH_NONE, STATE_OFF, STATE_ON, STATE_PAUSED, @@ -201,6 +202,10 @@ class DenonDevice(MediaPlayerDevice): def signal_handler(self, data): """Handle domain-specific signal by calling appropriate method.""" entity_ids = data[ATTR_ENTITY_ID] + + if entity_ids == ENTITY_MATCH_NONE: + return + if entity_ids == ENTITY_MATCH_ALL or self.entity_id in entity_ids: params = { key: value diff --git a/homeassistant/components/insteon/schemas.py b/homeassistant/components/insteon/schemas.py index 1ae4ebed99e..20399195365 100644 --- a/homeassistant/components/insteon/schemas.py +++ b/homeassistant/components/insteon/schemas.py @@ -11,6 +11,7 @@ from homeassistant.const import ( CONF_PLATFORM, CONF_PORT, ENTITY_MATCH_ALL, + ENTITY_MATCH_NONE, ) import homeassistant.helpers.config_validation as cv @@ -136,7 +137,9 @@ DEL_ALL_LINK_SCHEMA = vol.Schema( LOAD_ALDB_SCHEMA = vol.Schema( { - vol.Required(CONF_ENTITY_ID): vol.Any(cv.entity_id, ENTITY_MATCH_ALL), + vol.Required(CONF_ENTITY_ID): vol.Any( + cv.entity_id, ENTITY_MATCH_ALL, ENTITY_MATCH_NONE + ), vol.Optional(SRV_LOAD_DB_RELOAD, default=False): cv.boolean, } ) diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index 4e845a07854..5bc0c1bc53b 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -39,6 +39,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_MODE, ENTITY_MATCH_ALL, + ENTITY_MATCH_NONE, EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import callback @@ -374,6 +375,9 @@ class LIFXManager: async def async_service_to_entities(self, service): """Return the known entities that a service call mentions.""" + if service.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_NONE: + return [] + if service.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_ALL: return self.entities.values() diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 318101605e8..3a456dec531 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -22,7 +22,7 @@ from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SERVICE_PLAY_MEDIA, ) -from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM, ENTITY_MATCH_ALL +from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform, discovery @@ -90,7 +90,7 @@ SCHEMA_SERVICE_SAY = vol.Schema( { vol.Required(ATTR_MESSAGE): cv.string, vol.Optional(ATTR_CACHE): cv.boolean, - vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, + vol.Required(ATTR_ENTITY_ID): cv.comp_entity_ids, vol.Optional(ATTR_LANGUAGE): cv.string, vol.Optional(ATTR_OPTIONS): dict, } @@ -148,7 +148,7 @@ async def async_setup(hass, config): async def async_say_handle(service): """Service handle for say.""" - entity_ids = service.data.get(ATTR_ENTITY_ID, ENTITY_MATCH_ALL) + entity_ids = service.data[ATTR_ENTITY_ID] message = service.data.get(ATTR_MESSAGE) cache = service.data.get(ATTR_CACHE) language = service.data.get(ATTR_LANGUAGE) diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index 99df9fd17ce..f4d9f97fe42 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -29,6 +29,7 @@ from homeassistant.const import ( CONF_HOST, CONF_NAME, ENTITY_MATCH_ALL, + ENTITY_MATCH_NONE, STATE_OFF, STATE_ON, ) @@ -137,6 +138,9 @@ class LgWebOSMediaPlayerEntity(MediaPlayerDevice): async def async_signal_handler(self, data): """Handle domain-specific signal by calling appropriate method.""" entity_ids = data[ATTR_ENTITY_ID] + if entity_ids == ENTITY_MATCH_NONE: + return + if entity_ids == ENTITY_MATCH_ALL or self.entity_id in entity_ids: params = { key: value diff --git a/homeassistant/const.py b/homeassistant/const.py index facb365f75c..ee2c4767ba9 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -16,6 +16,7 @@ PLATFORM_FORMAT = "{platform}.{domain}" MATCH_ALL = "*" # Entity target all constant +ENTITY_MATCH_NONE = "none" ENTITY_MATCH_ALL = "all" # If no name is specified diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 852948220de..f1caf38bf8b 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -52,6 +52,7 @@ from homeassistant.const import ( CONF_UNIT_SYSTEM_METRIC, CONF_VALUE_TEMPLATE, ENTITY_MATCH_ALL, + ENTITY_MATCH_NONE, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, TEMP_CELSIUS, @@ -231,7 +232,9 @@ def entity_ids(value: Union[str, List]) -> List[str]: return [entity_id(ent_id) for ent_id in value] -comp_entity_ids = vol.Any(vol.All(vol.Lower, ENTITY_MATCH_ALL), entity_ids) +comp_entity_ids = vol.Any( + vol.All(vol.Lower, vol.Any(ENTITY_MATCH_ALL, ENTITY_MATCH_NONE)), entity_ids +) def entity_domain(domain: str) -> Callable[[Any], str]: @@ -736,7 +739,9 @@ def make_entity_service_schema( { **schema, vol.Optional(ATTR_ENTITY_ID): comp_entity_ids, - vol.Optional(ATTR_AREA_ID): vol.All(ensure_list, [str]), + vol.Optional(ATTR_AREA_ID): vol.Any( + ENTITY_MATCH_NONE, vol.All(ensure_list, [str]) + ), }, extra=extra, ), diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index b30cab3fbd4..51f881181af 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -7,7 +7,12 @@ from typing import Callable import voluptuous as vol from homeassistant.auth.permissions.const import CAT_ENTITIES, POLICY_CONTROL -from homeassistant.const import ATTR_AREA_ID, ATTR_ENTITY_ID, ENTITY_MATCH_ALL +from homeassistant.const import ( + ATTR_AREA_ID, + ATTR_ENTITY_ID, + ENTITY_MATCH_ALL, + ENTITY_MATCH_NONE, +) import homeassistant.core as ha from homeassistant.exceptions import ( HomeAssistantError, @@ -121,11 +126,25 @@ async def async_extract_entities(hass, entities, service_call, expand_group=True entity_ids = await async_extract_entity_ids(hass, service_call, expand_group) - return [ - entity - for entity in entities - if entity.available and entity.entity_id in entity_ids - ] + found = [] + + for entity in entities: + if entity.entity_id not in entity_ids: + continue + + entity_ids.remove(entity.entity_id) + + if not entity.available: + continue + + found.append(entity) + + if entity_ids: + _LOGGER.warning( + "Unable to find referenced entities %s", ", ".join(sorted(entity_ids)) + ) + + return found @bind_hass @@ -137,12 +156,15 @@ async def async_extract_entity_ids(hass, service_call, expand_group=True): entity_ids = service_call.data.get(ATTR_ENTITY_ID) area_ids = service_call.data.get(ATTR_AREA_ID) - if not entity_ids and not area_ids: - return [] - extracted = set() - if entity_ids: + if entity_ids in (None, ENTITY_MATCH_NONE) and area_ids in ( + None, + ENTITY_MATCH_NONE, + ): + return extracted + + if entity_ids and entity_ids != ENTITY_MATCH_NONE: # Entity ID attr can be a list or a string if isinstance(entity_ids, str): entity_ids = [entity_ids] @@ -152,7 +174,7 @@ async def async_extract_entity_ids(hass, service_call, expand_group=True): extracted.update(entity_ids) - if area_ids: + if area_ids and area_ids != ENTITY_MATCH_NONE: if isinstance(area_ids, str): area_ids = [area_ids] @@ -342,6 +364,16 @@ async def entity_service_call(hass, platforms, func, call, required_features=Non platforms_entities.append(platform_entities) + if not target_all_entities: + for platform_entities in platforms_entities: + for entity in platform_entities: + entity_ids.remove(entity.entity_id) + + if entity_ids: + _LOGGER.warning( + "Unable to find referenced entities %s", ", ".join(sorted(entity_ids)) + ) + tasks = [ _handle_service_platform_call( hass, func, data, entities, call.context, required_features diff --git a/tests/components/google_translate/test_tts.py b/tests/components/google_translate/test_tts.py index 15e84b384c0..37609e151bd 100644 --- a/tests/components/google_translate/test_tts.py +++ b/tests/components/google_translate/test_tts.py @@ -65,7 +65,10 @@ class TestTTSGooglePlatform: self.hass.services.call( tts.DOMAIN, "google_translate_say", - {tts.ATTR_MESSAGE: "90% of I person is on front of your door."}, + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "90% of I person is on front of your door.", + }, ) self.hass.block_till_done() @@ -89,7 +92,10 @@ class TestTTSGooglePlatform: self.hass.services.call( tts.DOMAIN, "google_translate_say", - {tts.ATTR_MESSAGE: "90% of I person is on front of your door."}, + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "90% of I person is on front of your door.", + }, ) self.hass.block_till_done() @@ -115,6 +121,7 @@ class TestTTSGooglePlatform: tts.DOMAIN, "google_say", { + "entity_id": "media_player.something", tts.ATTR_MESSAGE: "90% of I person is on front of your door.", tts.ATTR_LANGUAGE: "de", }, @@ -139,7 +146,10 @@ class TestTTSGooglePlatform: self.hass.services.call( tts.DOMAIN, "google_translate_say", - {tts.ATTR_MESSAGE: "90% of I person is on front of your door."}, + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "90% of I person is on front of your door.", + }, ) self.hass.block_till_done() @@ -161,7 +171,10 @@ class TestTTSGooglePlatform: self.hass.services.call( tts.DOMAIN, "google_translate_say", - {tts.ATTR_MESSAGE: "90% of I person is on front of your door."}, + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "90% of I person is on front of your door.", + }, ) self.hass.block_till_done() @@ -193,6 +206,7 @@ class TestTTSGooglePlatform: tts.DOMAIN, "google_say", { + "entity_id": "media_player.something", tts.ATTR_MESSAGE: ( "I person is on front of your door." "I person is on front of your door." @@ -203,7 +217,7 @@ class TestTTSGooglePlatform: "I person is on front of your door." "I person is on front of your door." "I person is on front of your door." - ) + ), }, ) self.hass.block_till_done() diff --git a/tests/components/marytts/test_tts.py b/tests/components/marytts/test_tts.py index 810998ec0b8..d8a96b2db52 100644 --- a/tests/components/marytts/test_tts.py +++ b/tests/components/marytts/test_tts.py @@ -66,7 +66,12 @@ class TestTTSMaryTTSPlatform: with patch("http.client.HTTPConnection", return_value=conn): self.hass.services.call( - tts.DOMAIN, "marytts_say", {tts.ATTR_MESSAGE: "HomeAssistant"} + tts.DOMAIN, + "marytts_say", + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "HomeAssistant", + }, ) self.hass.block_till_done() @@ -93,7 +98,12 @@ class TestTTSMaryTTSPlatform: with patch("http.client.HTTPConnection", return_value=conn): self.hass.services.call( - tts.DOMAIN, "marytts_say", {tts.ATTR_MESSAGE: "HomeAssistant"} + tts.DOMAIN, + "marytts_say", + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "HomeAssistant", + }, ) self.hass.block_till_done() @@ -123,7 +133,12 @@ class TestTTSMaryTTSPlatform: with patch("http.client.HTTPConnection", return_value=conn): self.hass.services.call( - tts.DOMAIN, "marytts_say", {tts.ATTR_MESSAGE: "HomeAssistant"} + tts.DOMAIN, + "marytts_say", + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "HomeAssistant", + }, ) self.hass.block_till_done() diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index 6aafe29901d..62c4bc3a065 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -95,7 +95,10 @@ class TestTTS: self.hass.services.call( tts.DOMAIN, "demo_say", - {tts.ATTR_MESSAGE: "I person is on front of your door."}, + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", + }, ) self.hass.block_till_done() @@ -103,13 +106,13 @@ class TestTTS: assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC assert calls[0].data[ ATTR_MEDIA_CONTENT_ID - ] == "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd_en_-_demo.mp3".format( + ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3".format( self.hass.config.api.base_url ) assert os.path.isfile( os.path.join( self.default_tts_cache, - "265944c108cbb00b2a621be5930513e03a0bb2cd_en_-_demo.mp3", + "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3", ) ) @@ -125,7 +128,10 @@ class TestTTS: self.hass.services.call( tts.DOMAIN, "demo_say", - {tts.ATTR_MESSAGE: "I person is on front of your door."}, + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", + }, ) self.hass.block_till_done() @@ -133,13 +139,13 @@ class TestTTS: assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC assert calls[0].data[ ATTR_MEDIA_CONTENT_ID - ] == "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd_de_-_demo.mp3".format( + ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3".format( self.hass.config.api.base_url ) assert os.path.isfile( os.path.join( self.default_tts_cache, - "265944c108cbb00b2a621be5930513e03a0bb2cd_de_-_demo.mp3", + "42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3", ) ) @@ -163,7 +169,8 @@ class TestTTS: tts.DOMAIN, "demo_say", { - tts.ATTR_MESSAGE: "I person is on front of your door.", + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", tts.ATTR_LANGUAGE: "de", }, ) @@ -173,13 +180,13 @@ class TestTTS: assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC assert calls[0].data[ ATTR_MEDIA_CONTENT_ID - ] == "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd_de_-_demo.mp3".format( + ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3".format( self.hass.config.api.base_url ) assert os.path.isfile( os.path.join( self.default_tts_cache, - "265944c108cbb00b2a621be5930513e03a0bb2cd_de_-_demo.mp3", + "42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3", ) ) @@ -196,7 +203,8 @@ class TestTTS: tts.DOMAIN, "demo_say", { - tts.ATTR_MESSAGE: "I person is on front of your door.", + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", tts.ATTR_LANGUAGE: "lang", }, ) @@ -206,7 +214,7 @@ class TestTTS: assert not os.path.isfile( os.path.join( self.default_tts_cache, - "265944c108cbb00b2a621be5930513e03a0bb2cd_lang_-_demo.mp3", + "42f18378fd4393d18c8dd11d03fa9563c1e54491_lang_-_demo.mp3", ) ) @@ -223,7 +231,8 @@ class TestTTS: tts.DOMAIN, "demo_say", { - tts.ATTR_MESSAGE: "I person is on front of your door.", + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", tts.ATTR_LANGUAGE: "de", tts.ATTR_OPTIONS: {"voice": "alex"}, }, @@ -236,13 +245,13 @@ class TestTTS: assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC assert calls[0].data[ ATTR_MEDIA_CONTENT_ID - ] == "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd_de_{}_demo.mp3".format( + ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{}_demo.mp3".format( self.hass.config.api.base_url, opt_hash ) assert os.path.isfile( os.path.join( self.default_tts_cache, - "265944c108cbb00b2a621be5930513e03a0bb2cd_de_{0}_demo.mp3".format( + "42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{0}_demo.mp3".format( opt_hash ), ) @@ -265,7 +274,8 @@ class TestTTS: tts.DOMAIN, "demo_say", { - tts.ATTR_MESSAGE: "I person is on front of your door.", + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", tts.ATTR_LANGUAGE: "de", }, ) @@ -277,13 +287,13 @@ class TestTTS: assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC assert calls[0].data[ ATTR_MEDIA_CONTENT_ID - ] == "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd_de_{}_demo.mp3".format( + ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{}_demo.mp3".format( self.hass.config.api.base_url, opt_hash ) assert os.path.isfile( os.path.join( self.default_tts_cache, - "265944c108cbb00b2a621be5930513e03a0bb2cd_de_{0}_demo.mp3".format( + "42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{0}_demo.mp3".format( opt_hash ), ) @@ -302,7 +312,8 @@ class TestTTS: tts.DOMAIN, "demo_say", { - tts.ATTR_MESSAGE: "I person is on front of your door.", + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", tts.ATTR_LANGUAGE: "de", tts.ATTR_OPTIONS: {"speed": 1}, }, @@ -315,7 +326,7 @@ class TestTTS: assert not os.path.isfile( os.path.join( self.default_tts_cache, - "265944c108cbb00b2a621be5930513e03a0bb2cd_de_{0}_demo.mp3".format( + "42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{0}_demo.mp3".format( opt_hash ), ) @@ -333,7 +344,10 @@ class TestTTS: self.hass.services.call( tts.DOMAIN, "demo_say", - {tts.ATTR_MESSAGE: "I person is on front of your door."}, + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", + }, ) self.hass.block_till_done() @@ -341,7 +355,7 @@ class TestTTS: assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC assert ( calls[0].data[ATTR_MEDIA_CONTENT_ID] == "http://fnord" - "/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd" + "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491" "_en_-_demo.mp3" ) @@ -357,7 +371,10 @@ class TestTTS: self.hass.services.call( tts.DOMAIN, "demo_say", - {tts.ATTR_MESSAGE: "I person is on front of your door."}, + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", + }, ) self.hass.block_till_done() @@ -365,7 +382,7 @@ class TestTTS: assert os.path.isfile( os.path.join( self.default_tts_cache, - "265944c108cbb00b2a621be5930513e03a0bb2cd_en_-_demo.mp3", + "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3", ) ) @@ -375,7 +392,7 @@ class TestTTS: assert not os.path.isfile( os.path.join( self.default_tts_cache, - "265944c108cbb00b2a621be5930513e03a0bb2cd_en_-_demo.mp3", + "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3", ) ) @@ -393,7 +410,10 @@ class TestTTS: self.hass.services.call( tts.DOMAIN, "demo_say", - {tts.ATTR_MESSAGE: "I person is on front of your door."}, + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", + }, ) self.hass.block_till_done() @@ -401,7 +421,7 @@ class TestTTS: req = requests.get(calls[0].data[ATTR_MEDIA_CONTENT_ID]) _, demo_data = self.demo_provider.get_tts_audio("bla", "en") demo_data = tts.SpeechManager.write_tags( - "265944c108cbb00b2a621be5930513e03a0bb2cd_en_-_demo.mp3", + "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3", demo_data, self.demo_provider, "AI person is in front of your door.", @@ -425,7 +445,10 @@ class TestTTS: self.hass.services.call( tts.DOMAIN, "demo_say", - {tts.ATTR_MESSAGE: "I person is on front of your door."}, + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", + }, ) self.hass.block_till_done() @@ -433,10 +456,10 @@ class TestTTS: req = requests.get(calls[0].data[ATTR_MEDIA_CONTENT_ID]) _, demo_data = self.demo_provider.get_tts_audio("bla", "de") demo_data = tts.SpeechManager.write_tags( - "265944c108cbb00b2a621be5930513e03a0bb2cd_de_-_demo.mp3", + "42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3", demo_data, self.demo_provider, - "I person is on front of your door.", + "There is someone at the door.", "de", None, ) @@ -453,7 +476,7 @@ class TestTTS: self.hass.start() url = ( - "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd_en_-_demo.mp3" + "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3" ).format(self.hass.config.api.base_url) req = requests.get(url) @@ -487,7 +510,10 @@ class TestTTS: self.hass.services.call( tts.DOMAIN, "demo_say", - {tts.ATTR_MESSAGE: "I person is on front of your door."}, + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", + }, ) self.hass.block_till_done() @@ -495,7 +521,7 @@ class TestTTS: assert not os.path.isfile( os.path.join( self.default_tts_cache, - "265944c108cbb00b2a621be5930513e03a0bb2cd_en_-_demo.mp3", + "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3", ) ) @@ -512,7 +538,8 @@ class TestTTS: tts.DOMAIN, "demo_say", { - tts.ATTR_MESSAGE: "I person is on front of your door.", + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", tts.ATTR_CACHE: False, }, ) @@ -522,7 +549,7 @@ class TestTTS: assert not os.path.isfile( os.path.join( self.default_tts_cache, - "265944c108cbb00b2a621be5930513e03a0bb2cd_en_-_demo.mp3", + "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3", ) ) @@ -533,7 +560,7 @@ class TestTTS: _, demo_data = self.demo_provider.get_tts_audio("bla", "en") cache_file = os.path.join( self.default_tts_cache, - "265944c108cbb00b2a621be5930513e03a0bb2cd_en_-_demo.mp3", + "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3", ) os.mkdir(self.default_tts_cache) @@ -552,14 +579,17 @@ class TestTTS: self.hass.services.call( tts.DOMAIN, "demo_say", - {tts.ATTR_MESSAGE: "I person is on front of your door."}, + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", + }, ) self.hass.block_till_done() assert len(calls) == 1 assert calls[0].data[ ATTR_MEDIA_CONTENT_ID - ] == "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd_en_-_demo.mp3".format( + ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3".format( self.hass.config.api.base_url ) @@ -579,7 +609,10 @@ class TestTTS: self.hass.services.call( tts.DOMAIN, "demo_say", - {tts.ATTR_MESSAGE: "I person is on front of your door."}, + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", + }, ) self.hass.block_till_done() @@ -590,7 +623,7 @@ class TestTTS: _, demo_data = self.demo_provider.get_tts_audio("bla", "en") cache_file = os.path.join( self.default_tts_cache, - "265944c108cbb00b2a621be5930513e03a0bb2cd_en_-_demo.mp3", + "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3", ) os.mkdir(self.default_tts_cache) @@ -605,7 +638,7 @@ class TestTTS: self.hass.start() url = ( - "{}/api/tts_proxy/265944c108cbb00b2a621be5930513e03a0bb2cd_en_-_demo.mp3" + "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3" ).format(self.hass.config.api.base_url) req = requests.get(url) @@ -622,14 +655,15 @@ async def test_setup_component_and_web_get_url(hass, hass_client): client = await hass_client() url = "/api/tts_get_url" - data = {"platform": "demo", "message": "I person is on front of your door."} + data = {"platform": "demo", "message": "There is someone at the door."} req = await client.post(url, json=data) assert req.status == 200 response = await req.json() assert response.get("url") == ( - "{}/api/tts_proxy/265944c108cbb00b2a62" - "1be5930513e03a0bb2cd_en_-_demo.mp3".format(hass.config.api.base_url) + "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3".format( + hass.config.api.base_url + ) ) tts_cache = hass.config.path(tts.DEFAULT_CACHE_DIR) @@ -646,7 +680,7 @@ async def test_setup_component_and_web_get_url_bad_config(hass, hass_client): client = await hass_client() url = "/api/tts_get_url" - data = {"message": "I person is on front of your door."} + data = {"message": "There is someone at the door."} req = await client.post(url, json=data) assert req.status == 400 diff --git a/tests/components/voicerss/test_tts.py b/tests/components/voicerss/test_tts.py index d2a7197fe1a..a65201735ae 100644 --- a/tests/components/voicerss/test_tts.py +++ b/tests/components/voicerss/test_tts.py @@ -67,7 +67,10 @@ class TestTTSVoiceRSSPlatform: self.hass.services.call( tts.DOMAIN, "voicerss_say", - {tts.ATTR_MESSAGE: "I person is on front of your door."}, + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "I person is on front of your door.", + }, ) self.hass.block_till_done() @@ -97,7 +100,10 @@ class TestTTSVoiceRSSPlatform: self.hass.services.call( tts.DOMAIN, "voicerss_say", - {tts.ATTR_MESSAGE: "I person is on front of your door."}, + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "I person is on front of your door.", + }, ) self.hass.block_till_done() @@ -121,6 +127,7 @@ class TestTTSVoiceRSSPlatform: tts.DOMAIN, "voicerss_say", { + "entity_id": "media_player.something", tts.ATTR_MESSAGE: "I person is on front of your door.", tts.ATTR_LANGUAGE: "de-de", }, @@ -145,7 +152,10 @@ class TestTTSVoiceRSSPlatform: self.hass.services.call( tts.DOMAIN, "voicerss_say", - {tts.ATTR_MESSAGE: "I person is on front of your door."}, + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "I person is on front of your door.", + }, ) self.hass.block_till_done() @@ -167,7 +177,10 @@ class TestTTSVoiceRSSPlatform: self.hass.services.call( tts.DOMAIN, "voicerss_say", - {tts.ATTR_MESSAGE: "I person is on front of your door."}, + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "I person is on front of your door.", + }, ) self.hass.block_till_done() @@ -194,7 +207,10 @@ class TestTTSVoiceRSSPlatform: self.hass.services.call( tts.DOMAIN, "voicerss_say", - {tts.ATTR_MESSAGE: "I person is on front of your door."}, + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "I person is on front of your door.", + }, ) self.hass.block_till_done() diff --git a/tests/components/yandextts/test_tts.py b/tests/components/yandextts/test_tts.py index edd5c058f12..182c629d795 100644 --- a/tests/components/yandextts/test_tts.py +++ b/tests/components/yandextts/test_tts.py @@ -67,7 +67,9 @@ class TestTTSYandexPlatform: setup_component(self.hass, tts.DOMAIN, config) self.hass.services.call( - tts.DOMAIN, "yandextts_say", {tts.ATTR_MESSAGE: "HomeAssistant"} + tts.DOMAIN, + "yandextts_say", + {"entity_id": "media_player.something", tts.ATTR_MESSAGE: "HomeAssistant"}, ) self.hass.block_till_done() @@ -103,7 +105,9 @@ class TestTTSYandexPlatform: setup_component(self.hass, tts.DOMAIN, config) self.hass.services.call( - tts.DOMAIN, "yandextts_say", {tts.ATTR_MESSAGE: "HomeAssistant"} + tts.DOMAIN, + "yandextts_say", + {"entity_id": "media_player.something", tts.ATTR_MESSAGE: "HomeAssistant"}, ) self.hass.block_till_done() @@ -135,7 +139,11 @@ class TestTTSYandexPlatform: self.hass.services.call( tts.DOMAIN, "yandextts_say", - {tts.ATTR_MESSAGE: "HomeAssistant", tts.ATTR_LANGUAGE: "ru-RU"}, + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "HomeAssistant", + tts.ATTR_LANGUAGE: "ru-RU", + }, ) self.hass.block_till_done() @@ -165,7 +173,9 @@ class TestTTSYandexPlatform: setup_component(self.hass, tts.DOMAIN, config) self.hass.services.call( - tts.DOMAIN, "yandextts_say", {tts.ATTR_MESSAGE: "HomeAssistant"} + tts.DOMAIN, + "yandextts_say", + {"entity_id": "media_player.something", tts.ATTR_MESSAGE: "HomeAssistant"}, ) self.hass.block_till_done() @@ -195,7 +205,9 @@ class TestTTSYandexPlatform: setup_component(self.hass, tts.DOMAIN, config) self.hass.services.call( - tts.DOMAIN, "yandextts_say", {tts.ATTR_MESSAGE: "HomeAssistant"} + tts.DOMAIN, + "yandextts_say", + {"entity_id": "media_player.something", tts.ATTR_MESSAGE: "HomeAssistant"}, ) self.hass.block_till_done() @@ -230,7 +242,9 @@ class TestTTSYandexPlatform: setup_component(self.hass, tts.DOMAIN, config) self.hass.services.call( - tts.DOMAIN, "yandextts_say", {tts.ATTR_MESSAGE: "HomeAssistant"} + tts.DOMAIN, + "yandextts_say", + {"entity_id": "media_player.something", tts.ATTR_MESSAGE: "HomeAssistant"}, ) self.hass.block_till_done() @@ -266,7 +280,9 @@ class TestTTSYandexPlatform: setup_component(self.hass, tts.DOMAIN, config) self.hass.services.call( - tts.DOMAIN, "yandextts_say", {tts.ATTR_MESSAGE: "HomeAssistant"} + tts.DOMAIN, + "yandextts_say", + {"entity_id": "media_player.something", tts.ATTR_MESSAGE: "HomeAssistant"}, ) self.hass.block_till_done() @@ -298,7 +314,9 @@ class TestTTSYandexPlatform: setup_component(self.hass, tts.DOMAIN, config) self.hass.services.call( - tts.DOMAIN, "yandextts_say", {tts.ATTR_MESSAGE: "HomeAssistant"} + tts.DOMAIN, + "yandextts_say", + {"entity_id": "media_player.something", tts.ATTR_MESSAGE: "HomeAssistant"}, ) self.hass.block_till_done() @@ -330,7 +348,9 @@ class TestTTSYandexPlatform: setup_component(self.hass, tts.DOMAIN, config) self.hass.services.call( - tts.DOMAIN, "yandextts_say", {tts.ATTR_MESSAGE: "HomeAssistant"} + tts.DOMAIN, + "yandextts_say", + {"entity_id": "media_player.something", tts.ATTR_MESSAGE: "HomeAssistant"}, ) self.hass.block_till_done() @@ -362,6 +382,7 @@ class TestTTSYandexPlatform: tts.DOMAIN, "yandextts_say", { + "entity_id": "media_player.something", tts.ATTR_MESSAGE: "HomeAssistant", "options": {"emotion": "evil", "speed": 2}, }, diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 41f84e65f6c..306402cd2b9 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -7,8 +7,9 @@ from unittest.mock import Mock, patch import asynctest import pytest +import voluptuous as vol -from homeassistant.const import ENTITY_MATCH_ALL +from homeassistant.const import ENTITY_MATCH_ALL, ENTITY_MATCH_NONE import homeassistant.core as ha from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import discovery @@ -223,10 +224,21 @@ async def test_extract_from_service_fails_if_no_entity_id(hass): [MockEntity(name="test_1"), MockEntity(name="test_2")] ) - call = ha.ServiceCall("test", "service") - - assert [] == sorted( - ent.entity_id for ent in (await component.async_extract_from_service(call)) + assert ( + await component.async_extract_from_service(ha.ServiceCall("test", "service")) + == [] + ) + assert ( + await component.async_extract_from_service( + ha.ServiceCall("test", "service", {"entity_id": ENTITY_MATCH_NONE}) + ) + == [] + ) + assert ( + await component.async_extract_from_service( + ha.ServiceCall("test", "service", {"area_id": ENTITY_MATCH_NONE}) + ) + == [] ) @@ -429,3 +441,53 @@ async def test_extract_all_use_match_all(hass, caplog): assert ( "Not passing an entity ID to a service to target all entities is deprecated" ) not in caplog.text + + +async def test_register_entity_service(hass): + """Test not expanding a group.""" + entity = MockEntity(entity_id=f"{DOMAIN}.entity") + calls = [] + + @ha.callback + def appender(**kwargs): + calls.append(kwargs) + + entity.async_called_by_service = appender + + component = EntityComponent(_LOGGER, DOMAIN, hass) + await component.async_add_entities([entity]) + + component.async_register_entity_service( + "hello", {"some": str}, "async_called_by_service" + ) + + with pytest.raises(vol.Invalid): + await hass.services.async_call( + DOMAIN, + "hello", + {"entity_id": entity.entity_id, "invalid": "data"}, + blocking=True, + ) + assert len(calls) == 0 + + await hass.services.async_call( + DOMAIN, "hello", {"entity_id": entity.entity_id, "some": "data"}, blocking=True + ) + assert len(calls) == 1 + assert calls[0] == {"some": "data"} + + await hass.services.async_call( + DOMAIN, "hello", {"entity_id": ENTITY_MATCH_ALL, "some": "data"}, blocking=True + ) + assert len(calls) == 2 + assert calls[1] == {"some": "data"} + + await hass.services.async_call( + DOMAIN, "hello", {"entity_id": ENTITY_MATCH_NONE, "some": "data"}, blocking=True + ) + assert len(calls) == 2 + + await hass.services.async_call( + DOMAIN, "hello", {"area_id": ENTITY_MATCH_NONE, "some": "data"}, blocking=True + ) + assert len(calls) == 2 diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index 5585437867c..106fdfabf2d 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -12,7 +12,13 @@ import voluptuous as vol from homeassistant import core as ha, exceptions from homeassistant.auth.permissions import PolicyPermissions import homeassistant.components # noqa: F401 -from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, STATE_OFF, STATE_ON +from homeassistant.const import ( + ATTR_ENTITY_ID, + ENTITY_MATCH_ALL, + ENTITY_MATCH_NONE, + STATE_OFF, + STATE_ON, +) from homeassistant.helpers import ( device_registry as dev_reg, entity_registry as ent_reg, @@ -252,6 +258,14 @@ async def test_extract_entity_ids(hass): hass, call, expand_group=False ) + assert ( + await service.async_extract_entity_ids( + hass, + ha.ServiceCall("light", "turn_on", {ATTR_ENTITY_ID: ENTITY_MATCH_NONE}), + ) + == set() + ) + async def test_extract_entity_ids_from_area(hass, area_mock): """Test extract_entity_ids method with areas.""" @@ -266,6 +280,13 @@ async def test_extract_entity_ids_from_area(hass, area_mock): "light.diff_area", } == await service.async_extract_entity_ids(hass, call) + assert ( + await service.async_extract_entity_ids( + hass, ha.ServiceCall("light", "turn_on", {"area_id": ENTITY_MATCH_NONE}) + ) + == set() + ) + @asyncio.coroutine def test_async_get_all_descriptions(hass): @@ -742,6 +763,15 @@ async def test_extract_from_service_available_device(hass): for ent in (await service.async_extract_entities(hass, entities, call_2)) ] + assert ( + await service.async_extract_entities( + hass, + entities, + ha.ServiceCall("test", "service", data={"entity_id": ENTITY_MATCH_NONE},), + ) + == [] + ) + async def test_extract_from_service_empty_if_no_entity_id(hass): """Test the extraction from service without specifying entity."""