From 62bc8df9640ddbadc6b9da0c2713b87ee4b2028b Mon Sep 17 00:00:00 2001 From: Pascal Reeb Date: Tue, 11 Apr 2023 11:13:52 +0200 Subject: [PATCH 01/10] Fall back to polling if webhook cannot be registered on Nuki (#91013) fix(nuki): throw warning if webhook cannot be created --- homeassistant/components/nuki/__init__.py | 159 ++++++++++++---------- homeassistant/components/nuki/helpers.py | 4 + 2 files changed, 89 insertions(+), 74 deletions(-) diff --git a/homeassistant/components/nuki/__init__.py b/homeassistant/components/nuki/__init__.py index ef168374bd8..b0bfe18614e 100644 --- a/homeassistant/components/nuki/__init__.py +++ b/homeassistant/components/nuki/__init__.py @@ -25,7 +25,6 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import Event, HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import ( device_registry as dr, entity_registry as er, @@ -47,7 +46,7 @@ from .const import ( DOMAIN, ERROR_STATES, ) -from .helpers import parse_id +from .helpers import NukiWebhookException, parse_id _NukiDeviceT = TypeVar("_NukiDeviceT", bound=NukiDevice) @@ -61,6 +60,87 @@ def _get_bridge_devices(bridge: NukiBridge) -> tuple[list[NukiLock], list[NukiOp return bridge.locks, bridge.openers +async def _create_webhook( + hass: HomeAssistant, entry: ConfigEntry, bridge: NukiBridge +) -> None: + # Create HomeAssistant webhook + async def handle_webhook( + hass: HomeAssistant, webhook_id: str, request: web.Request + ) -> web.Response: + """Handle webhook callback.""" + try: + data = await request.json() + except ValueError: + return web.Response(status=HTTPStatus.BAD_REQUEST) + + locks = hass.data[DOMAIN][entry.entry_id][DATA_LOCKS] + openers = hass.data[DOMAIN][entry.entry_id][DATA_OPENERS] + + devices = [x for x in locks + openers if x.nuki_id == data["nukiId"]] + if len(devices) == 1: + devices[0].update_from_callback(data) + + coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] + coordinator.async_set_updated_data(None) + + return web.Response(status=HTTPStatus.OK) + + webhook.async_register( + hass, DOMAIN, entry.title, entry.entry_id, handle_webhook, local_only=True + ) + + webhook_url = webhook.async_generate_path(entry.entry_id) + + try: + hass_url = get_url( + hass, + allow_cloud=False, + allow_external=False, + allow_ip=True, + require_ssl=False, + ) + except NoURLAvailableError: + webhook.async_unregister(hass, entry.entry_id) + raise NukiWebhookException( + f"Error registering URL for webhook {entry.entry_id}: " + "HomeAssistant URL is not available" + ) from None + + url = f"{hass_url}{webhook_url}" + + if hass_url.startswith("https"): + ir.async_create_issue( + hass, + DOMAIN, + "https_webhook", + is_fixable=False, + severity=ir.IssueSeverity.WARNING, + translation_key="https_webhook", + translation_placeholders={ + "base_url": hass_url, + "network_link": "https://my.home-assistant.io/redirect/network/", + }, + ) + else: + ir.async_delete_issue(hass, DOMAIN, "https_webhook") + + try: + async with async_timeout.timeout(10): + await hass.async_add_executor_job( + _register_webhook, bridge, entry.entry_id, url + ) + except InvalidCredentialsException as err: + webhook.async_unregister(hass, entry.entry_id) + raise NukiWebhookException( + f"Invalid credentials for Bridge: {err}" + ) from err + except RequestException as err: + webhook.async_unregister(hass, entry.entry_id) + raise NukiWebhookException( + f"Error communicating with Bridge: {err}" + ) from err + + def _register_webhook(bridge: NukiBridge, entry_id: str, url: str) -> bool: # Register HA URL as webhook if not already callbacks = bridge.callback_list() @@ -126,79 +206,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: sw_version=info["versions"]["firmwareVersion"], ) - async def handle_webhook( - hass: HomeAssistant, webhook_id: str, request: web.Request - ) -> web.Response: - """Handle webhook callback.""" - try: - data = await request.json() - except ValueError: - return web.Response(status=HTTPStatus.BAD_REQUEST) - - locks = hass.data[DOMAIN][entry.entry_id][DATA_LOCKS] - openers = hass.data[DOMAIN][entry.entry_id][DATA_OPENERS] - - devices = [x for x in locks + openers if x.nuki_id == data["nukiId"]] - if len(devices) == 1: - devices[0].update_from_callback(data) - - coordinator = hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR] - coordinator.async_set_updated_data(None) - - return web.Response(status=HTTPStatus.OK) - - webhook.async_register( - hass, DOMAIN, entry.title, entry.entry_id, handle_webhook, local_only=True - ) - - webhook_url = webhook.async_generate_path(entry.entry_id) - try: - hass_url = get_url( - hass, - allow_cloud=False, - allow_external=False, - allow_ip=True, - require_ssl=False, - ) - except NoURLAvailableError: - webhook.async_unregister(hass, entry.entry_id) - raise ConfigEntryNotReady( - f"Error registering URL for webhook {entry.entry_id}: " - "HomeAssistant URL is not available" - ) from None - - url = f"{hass_url}{webhook_url}" - - if hass_url.startswith("https"): - ir.async_create_issue( - hass, - DOMAIN, - "https_webhook", - is_fixable=False, - severity=ir.IssueSeverity.WARNING, - translation_key="https_webhook", - translation_placeholders={ - "base_url": hass_url, - "network_link": "https://my.home-assistant.io/redirect/network/", - }, - ) - else: - ir.async_delete_issue(hass, DOMAIN, "https_webhook") - - try: - async with async_timeout.timeout(10): - await hass.async_add_executor_job( - _register_webhook, bridge, entry.entry_id, url - ) - except InvalidCredentialsException as err: - webhook.async_unregister(hass, entry.entry_id) - raise ConfigEntryNotReady(f"Invalid credentials for Bridge: {err}") from err - except RequestException as err: - webhook.async_unregister(hass, entry.entry_id) - raise ConfigEntryNotReady( - f"Error communicating with Bridge: {err}" - ) from err + await _create_webhook(hass, entry, bridge) + except NukiWebhookException as err: + _LOGGER.warning("Error registering HomeAssistant webhook: %s", err) async def _stop_nuki(_: Event): """Stop and remove the Nuki webhook.""" diff --git a/homeassistant/components/nuki/helpers.py b/homeassistant/components/nuki/helpers.py index 45b7420754a..1ba8e393f54 100644 --- a/homeassistant/components/nuki/helpers.py +++ b/homeassistant/components/nuki/helpers.py @@ -13,3 +13,7 @@ class CannotConnect(exceptions.HomeAssistantError): class InvalidAuth(exceptions.HomeAssistantError): """Error to indicate there is invalid auth.""" + + +class NukiWebhookException(exceptions.HomeAssistantError): + """Error to indicate there was an issue with the webhook.""" From 0d7711f7875986d1234e0476f43f25736b1c19a5 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 11 Apr 2023 11:56:55 +0200 Subject: [PATCH 02/10] Fix UniFi client tracker host_name missing (#91188) --- homeassistant/components/unifi/device_tracker.py | 3 +-- tests/components/unifi/test_device_tracker.py | 3 +++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 149f865e776..296857e1cfa 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -54,7 +54,6 @@ CLIENT_CONNECTED_ATTRIBUTES = [ ] CLIENT_STATIC_ATTRIBUTES = [ - "hostname", "mac", "name", "oui", @@ -175,7 +174,7 @@ ENTITY_DESCRIPTIONS: tuple[UnifiTrackerEntityDescription, ...] = ( supported_fn=lambda controller, obj_id: True, unique_id_fn=lambda controller, obj_id: f"{obj_id}-{controller.site}", ip_address_fn=lambda api, obj_id: api.clients[obj_id].ip, - hostname_fn=lambda api, obj_id: None, + hostname_fn=lambda api, obj_id: api.clients[obj_id].hostname, ), UnifiTrackerEntityDescription[Devices, Device]( key="Device scanner", diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 1e68b497111..16432ff514e 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -144,6 +144,9 @@ async def test_tracked_clients( assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 4 assert hass.states.get("device_tracker.client_1").state == STATE_NOT_HOME assert hass.states.get("device_tracker.client_2").state == STATE_NOT_HOME + assert ( + hass.states.get("device_tracker.client_5").attributes["host_name"] == "client_5" + ) # Client on SSID not in SSID filter assert not hass.states.get("device_tracker.client_3") From 3c8397a7b9e863a8c7925865c766054627ab07c7 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 11 Apr 2023 14:33:08 +0200 Subject: [PATCH 03/10] Flush conversation name cache when an entity is renamed (#91214) --- .../components/conversation/default_agent.py | 7 +- tests/components/conversation/test_init.py | 286 +++++++++++++++++- 2 files changed, 289 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index 49569f66ac0..98959320d7a 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -32,6 +32,7 @@ from .const import DEFAULT_EXPOSED_ATTRIBUTES, DEFAULT_EXPOSED_DOMAINS, DOMAIN _LOGGER = logging.getLogger(__name__) _DEFAULT_ERROR_TEXT = "Sorry, I couldn't understand that" +_ENTITY_REGISTRY_UPDATE_FIELDS = ["aliases", "name", "original_name"] REGEX_TYPE = type(re.compile("")) @@ -450,8 +451,10 @@ class DefaultAgent(AbstractConversationAgent): @core.callback def _async_handle_entity_registry_changed(self, event: core.Event) -> None: - """Clear names list cache when an entity changes aliases.""" - if event.data["action"] == "update" and "aliases" not in event.data["changes"]: + """Clear names list cache when an entity registry entry has changed.""" + if event.data["action"] == "update" and not any( + field in event.data["changes"] for field in _ENTITY_REGISTRY_UPDATE_FIELDS + ): return self._slot_lists = None diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index eb38d875bfa..91bf444123a 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -148,7 +148,7 @@ async def test_http_processing_intent_target_ha_agent( } -async def test_http_processing_intent_entity_added( +async def test_http_processing_intent_entity_added_removed( hass: HomeAssistant, init_components, hass_client: ClientSessionGenerator, @@ -198,7 +198,7 @@ async def test_http_processing_intent_entity_added( "conversation_id": None, } - # Add an alias + # Add an entity entity_registry.async_get_or_create( "light", "demo", "5678", suggested_object_id="late" ) @@ -294,6 +294,288 @@ async def test_http_processing_intent_entity_added( } +async def test_http_processing_intent_alias_added_removed( + hass: HomeAssistant, + init_components, + hass_client: ClientSessionGenerator, + hass_admin_user: MockUser, + entity_registry: er.EntityRegistry, +) -> None: + """Test processing intent via HTTP API with aliases added later. + + We want to ensure that adding an alias later busts the cache + so that the new alias is available. + """ + entity_registry.async_get_or_create( + "light", "demo", "1234", suggested_object_id="kitchen" + ) + hass.states.async_set("light.kitchen", "off", {"friendly_name": "kitchen light"}) + + calls = async_mock_service(hass, LIGHT_DOMAIN, "turn_on") + client = await hass_client() + resp = await client.post( + "/api/conversation/process", json={"text": "turn on kitchen light"} + ) + + assert resp.status == HTTPStatus.OK + assert len(calls) == 1 + data = await resp.json() + + assert data == { + "response": { + "response_type": "action_done", + "card": {}, + "speech": { + "plain": { + "extra_data": None, + "speech": "Turned on light", + } + }, + "language": hass.config.language, + "data": { + "targets": [], + "success": [ + {"id": "light.kitchen", "name": "kitchen light", "type": "entity"} + ], + "failed": [], + }, + }, + "conversation_id": None, + } + + # Add an alias + entity_registry.async_update_entity("light.kitchen", aliases={"late added alias"}) + + client = await hass_client() + resp = await client.post( + "/api/conversation/process", json={"text": "turn on late added alias"} + ) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + assert data == { + "response": { + "response_type": "action_done", + "card": {}, + "speech": { + "plain": { + "extra_data": None, + "speech": "Turned on light", + } + }, + "language": hass.config.language, + "data": { + "targets": [], + "success": [ + {"id": "light.kitchen", "name": "kitchen light", "type": "entity"} + ], + "failed": [], + }, + }, + "conversation_id": None, + } + + # Now remove the alieas + entity_registry.async_update_entity("light.kitchen", aliases={}) + + client = await hass_client() + resp = await client.post( + "/api/conversation/process", json={"text": "turn on late added alias"} + ) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + assert data == { + "conversation_id": None, + "response": { + "card": {}, + "data": {"code": "no_intent_match"}, + "language": hass.config.language, + "response_type": "error", + "speech": { + "plain": { + "extra_data": None, + "speech": "Sorry, I couldn't understand that", + } + }, + }, + } + + +async def test_http_processing_intent_entity_renamed( + hass: HomeAssistant, + init_components, + hass_client: ClientSessionGenerator, + hass_admin_user: MockUser, + entity_registry: er.EntityRegistry, + enable_custom_integrations: None, +) -> None: + """Test processing intent via HTTP API with entities renamed later. + + We want to ensure that renaming an entity later busts the cache + so that the new name is used. + """ + platform = getattr(hass.components, "test.light") + platform.init(empty=True) + + entity = platform.MockLight("kitchen light", "on") + entity._attr_unique_id = "1234" + entity.entity_id = "light.kitchen" + platform.ENTITIES.append(entity) + assert await async_setup_component( + hass, + LIGHT_DOMAIN, + {LIGHT_DOMAIN: [{"platform": "test"}]}, + ) + + calls = async_mock_service(hass, LIGHT_DOMAIN, "turn_on") + client = await hass_client() + resp = await client.post( + "/api/conversation/process", json={"text": "turn on kitchen light"} + ) + + assert resp.status == HTTPStatus.OK + assert len(calls) == 1 + data = await resp.json() + + assert data == { + "response": { + "response_type": "action_done", + "card": {}, + "speech": { + "plain": { + "extra_data": None, + "speech": "Turned on light", + } + }, + "language": hass.config.language, + "data": { + "targets": [], + "success": [ + {"id": "light.kitchen", "name": "kitchen light", "type": "entity"} + ], + "failed": [], + }, + }, + "conversation_id": None, + } + + # Rename the entity + entity_registry.async_update_entity("light.kitchen", name="renamed light") + await hass.async_block_till_done() + + client = await hass_client() + resp = await client.post( + "/api/conversation/process", json={"text": "turn on renamed light"} + ) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + assert data == { + "response": { + "response_type": "action_done", + "card": {}, + "speech": { + "plain": { + "extra_data": None, + "speech": "Turned on light", + } + }, + "language": hass.config.language, + "data": { + "targets": [], + "success": [ + {"id": "light.kitchen", "name": "renamed light", "type": "entity"} + ], + "failed": [], + }, + }, + "conversation_id": None, + } + + client = await hass_client() + resp = await client.post( + "/api/conversation/process", json={"text": "turn on kitchen light"} + ) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + assert data == { + "conversation_id": None, + "response": { + "card": {}, + "data": {"code": "no_intent_match"}, + "language": hass.config.language, + "response_type": "error", + "speech": { + "plain": { + "extra_data": None, + "speech": "Sorry, I couldn't understand that", + } + }, + }, + } + + # Now clear the custom name + entity_registry.async_update_entity("light.kitchen", name=None) + await hass.async_block_till_done() + + client = await hass_client() + resp = await client.post( + "/api/conversation/process", json={"text": "turn on kitchen light"} + ) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + assert data == { + "response": { + "response_type": "action_done", + "card": {}, + "speech": { + "plain": { + "extra_data": None, + "speech": "Turned on light", + } + }, + "language": hass.config.language, + "data": { + "targets": [], + "success": [ + {"id": "light.kitchen", "name": "kitchen light", "type": "entity"} + ], + "failed": [], + }, + }, + "conversation_id": None, + } + + client = await hass_client() + resp = await client.post( + "/api/conversation/process", json={"text": "turn on renamed light"} + ) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + assert data == { + "conversation_id": None, + "response": { + "card": {}, + "data": {"code": "no_intent_match"}, + "language": hass.config.language, + "response_type": "error", + "speech": { + "plain": { + "extra_data": None, + "speech": "Sorry, I couldn't understand that", + } + }, + }, + } + + @pytest.mark.parametrize("agent_id", AGENT_ID_OPTIONS) @pytest.mark.parametrize("sentence", ("turn on kitchen", "turn kitchen on")) async def test_turn_on_intent( From e65da42a39e385c1c0e63c075608118e20b419d7 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 11 Apr 2023 14:35:08 +0200 Subject: [PATCH 04/10] Update frontend to 20230411.0 (#91219) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index b1fd062032f..a3af9f863ea 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20230406.1"] + "requirements": ["home-assistant-frontend==20230411.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e648d7aca7d..90733155df8 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -25,7 +25,7 @@ ha-av==10.0.0 hass-nabucasa==0.63.1 hassil==1.0.6 home-assistant-bluetooth==1.9.3 -home-assistant-frontend==20230406.1 +home-assistant-frontend==20230411.0 home-assistant-intents==2023.3.29 httpx==0.23.3 ifaddr==0.1.7 diff --git a/requirements_all.txt b/requirements_all.txt index 50215967805..f056fffc08d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -907,7 +907,7 @@ hole==0.8.0 holidays==0.21.13 # homeassistant.components.frontend -home-assistant-frontend==20230406.1 +home-assistant-frontend==20230411.0 # homeassistant.components.conversation home-assistant-intents==2023.3.29 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 47ba479ab23..1d2799643f5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -693,7 +693,7 @@ hole==0.8.0 holidays==0.21.13 # homeassistant.components.frontend -home-assistant-frontend==20230406.1 +home-assistant-frontend==20230411.0 # homeassistant.components.conversation home-assistant-intents==2023.3.29 From d14e96942dc6ed131be1c10e1571b57d5c631741 Mon Sep 17 00:00:00 2001 From: Regev Brody Date: Tue, 11 Apr 2023 16:16:26 +0300 Subject: [PATCH 05/10] Bump aioswitcher to 3.3.0 (#91215) fix: #85096 Switcher "No devices found on the network" --- homeassistant/components/switcher_kis/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switcher_kis/manifest.json b/homeassistant/components/switcher_kis/manifest.json index 422adf6c511..2be541c8106 100644 --- a/homeassistant/components/switcher_kis/manifest.json +++ b/homeassistant/components/switcher_kis/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_push", "loggers": ["aioswitcher"], "quality_scale": "platinum", - "requirements": ["aioswitcher==3.2.1"] + "requirements": ["aioswitcher==3.3.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index f056fffc08d..9cbc688067a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -282,7 +282,7 @@ aiosomecomfort==0.0.14 aiosteamist==0.3.2 # homeassistant.components.switcher_kis -aioswitcher==3.2.1 +aioswitcher==3.3.0 # homeassistant.components.syncthing aiosyncthing==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1d2799643f5..feed04103b8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -263,7 +263,7 @@ aiosomecomfort==0.0.14 aiosteamist==0.3.2 # homeassistant.components.switcher_kis -aioswitcher==3.2.1 +aioswitcher==3.3.0 # homeassistant.components.syncthing aiosyncthing==0.5.1 From e418c66d69e751b160332ddf0961e62e4c146cc7 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 11 Apr 2023 17:01:39 +0200 Subject: [PATCH 06/10] Update spotipy to 2.23.0 (#91217) --- homeassistant/components/spotify/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/spotify/manifest.json b/homeassistant/components/spotify/manifest.json index 085146d4eff..7ca1533744c 100644 --- a/homeassistant/components/spotify/manifest.json +++ b/homeassistant/components/spotify/manifest.json @@ -9,6 +9,6 @@ "iot_class": "cloud_polling", "loggers": ["spotipy"], "quality_scale": "silver", - "requirements": ["spotipy==2.22.1"], + "requirements": ["spotipy==2.23.0"], "zeroconf": ["_spotify-connect._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 9cbc688067a..57aed880c0c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2388,7 +2388,7 @@ speedtest-cli==2.1.3 spiderpy==1.6.1 # homeassistant.components.spotify -spotipy==2.22.1 +spotipy==2.23.0 # homeassistant.components.recorder # homeassistant.components.sql diff --git a/requirements_test_all.txt b/requirements_test_all.txt index feed04103b8..54d2c3bb5c4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1709,7 +1709,7 @@ speedtest-cli==2.1.3 spiderpy==1.6.1 # homeassistant.components.spotify -spotipy==2.22.1 +spotipy==2.23.0 # homeassistant.components.recorder # homeassistant.components.sql From ca101cc7d14899c9db9a85c0c134873e22c33ae9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 11 Apr 2023 17:01:56 +0200 Subject: [PATCH 07/10] Update Pillow to 9.5.0 (#91218) --- homeassistant/components/doods/manifest.json | 2 +- homeassistant/components/generic/manifest.json | 2 +- homeassistant/components/image_upload/manifest.json | 2 +- homeassistant/components/proxy/manifest.json | 2 +- homeassistant/components/qrcode/manifest.json | 2 +- homeassistant/components/seven_segments/manifest.json | 2 +- homeassistant/components/sighthound/manifest.json | 2 +- homeassistant/components/tensorflow/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/doods/manifest.json b/homeassistant/components/doods/manifest.json index 5ffa1d4f290..79c114e2f38 100644 --- a/homeassistant/components/doods/manifest.json +++ b/homeassistant/components/doods/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/doods", "iot_class": "local_polling", "loggers": ["pydoods"], - "requirements": ["pydoods==1.0.2", "pillow==9.4.0"] + "requirements": ["pydoods==1.0.2", "pillow==9.5.0"] } diff --git a/homeassistant/components/generic/manifest.json b/homeassistant/components/generic/manifest.json index 0b1e040c79b..693959561d2 100644 --- a/homeassistant/components/generic/manifest.json +++ b/homeassistant/components/generic/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["http"], "documentation": "https://www.home-assistant.io/integrations/generic", "iot_class": "local_push", - "requirements": ["ha-av==10.0.0", "pillow==9.4.0"] + "requirements": ["ha-av==10.0.0", "pillow==9.5.0"] } diff --git a/homeassistant/components/image_upload/manifest.json b/homeassistant/components/image_upload/manifest.json index b53fb8bb292..947c3cb67d5 100644 --- a/homeassistant/components/image_upload/manifest.json +++ b/homeassistant/components/image_upload/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/image_upload", "integration_type": "system", "quality_scale": "internal", - "requirements": ["pillow==9.4.0"] + "requirements": ["pillow==9.5.0"] } diff --git a/homeassistant/components/proxy/manifest.json b/homeassistant/components/proxy/manifest.json index ae977b16a45..7ebaa6e53dd 100644 --- a/homeassistant/components/proxy/manifest.json +++ b/homeassistant/components/proxy/manifest.json @@ -3,5 +3,5 @@ "name": "Camera Proxy", "codeowners": [], "documentation": "https://www.home-assistant.io/integrations/proxy", - "requirements": ["pillow==9.4.0"] + "requirements": ["pillow==9.5.0"] } diff --git a/homeassistant/components/qrcode/manifest.json b/homeassistant/components/qrcode/manifest.json index 86bdf6c2dc7..787255187cc 100644 --- a/homeassistant/components/qrcode/manifest.json +++ b/homeassistant/components/qrcode/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/qrcode", "iot_class": "calculated", "loggers": ["pyzbar"], - "requirements": ["pillow==9.4.0", "pyzbar==0.1.7"] + "requirements": ["pillow==9.5.0", "pyzbar==0.1.7"] } diff --git a/homeassistant/components/seven_segments/manifest.json b/homeassistant/components/seven_segments/manifest.json index 58fe7a4de0b..90c5bf59fa3 100644 --- a/homeassistant/components/seven_segments/manifest.json +++ b/homeassistant/components/seven_segments/manifest.json @@ -4,5 +4,5 @@ "codeowners": ["@fabaff"], "documentation": "https://www.home-assistant.io/integrations/seven_segments", "iot_class": "local_polling", - "requirements": ["pillow==9.4.0"] + "requirements": ["pillow==9.5.0"] } diff --git a/homeassistant/components/sighthound/manifest.json b/homeassistant/components/sighthound/manifest.json index a4f024a8c03..1b6fbe9548d 100644 --- a/homeassistant/components/sighthound/manifest.json +++ b/homeassistant/components/sighthound/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/sighthound", "iot_class": "cloud_polling", "loggers": ["simplehound"], - "requirements": ["pillow==9.4.0", "simplehound==0.3"] + "requirements": ["pillow==9.5.0", "simplehound==0.3"] } diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index 668467c88d4..2178930199d 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -10,6 +10,6 @@ "tf-models-official==2.5.0", "pycocotools==2.0.1", "numpy==1.23.2", - "pillow==9.4.0" + "pillow==9.5.0" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 90733155df8..395b3700811 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -34,7 +34,7 @@ jinja2==3.1.2 lru-dict==1.1.8 orjson==3.8.10 paho-mqtt==1.6.1 -pillow==9.4.0 +pillow==9.5.0 pip>=21.0,<23.1 psutil-home-assistant==0.0.1 pyOpenSSL==23.1.0 diff --git a/requirements_all.txt b/requirements_all.txt index 57aed880c0c..dba4e7e856d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1355,7 +1355,7 @@ pilight==0.1.1 # homeassistant.components.seven_segments # homeassistant.components.sighthound # homeassistant.components.tensorflow -pillow==9.4.0 +pillow==9.5.0 # homeassistant.components.dominos pizzapi==0.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 54d2c3bb5c4..fba182b1d67 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -997,7 +997,7 @@ pilight==0.1.1 # homeassistant.components.seven_segments # homeassistant.components.sighthound # homeassistant.components.tensorflow -pillow==9.4.0 +pillow==9.5.0 # homeassistant.components.plex plexapi==4.13.2 From aa68d1d61752927e284efd545880b9b2d19f56f5 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 11 Apr 2023 17:41:38 +0200 Subject: [PATCH 08/10] Cleanup mqtt CONFIG_SCHEMA_ENTRY (#90791) --- homeassistant/components/mqtt/__init__.py | 23 ++++---- homeassistant/components/mqtt/client.py | 37 ++++++------ homeassistant/components/mqtt/config_flow.py | 2 - .../components/mqtt/config_integration.py | 50 ---------------- homeassistant/components/mqtt/const.py | 1 + tests/components/mqtt/test_diagnostics.py | 15 +---- tests/components/mqtt/test_init.py | 57 +++---------------- 7 files changed, 36 insertions(+), 149 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 7dbf7656d37..951b7bc25d4 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -24,7 +24,7 @@ from homeassistant.const import ( SERVICE_RELOAD, ) from homeassistant.core import HassJob, HomeAssistant, ServiceCall, callback -from homeassistant.exceptions import ConfigEntryError, TemplateError, Unauthorized +from homeassistant.exceptions import TemplateError, Unauthorized from homeassistant.helpers import config_validation as cv, event, template from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -45,7 +45,7 @@ from .client import ( # noqa: F401 publish, subscribe, ) -from .config_integration import CONFIG_SCHEMA_ENTRY, PLATFORM_CONFIG_SCHEMA_BASE +from .config_integration import PLATFORM_CONFIG_SCHEMA_BASE from .const import ( # noqa: F401 ATTR_PAYLOAD, ATTR_QOS, @@ -68,7 +68,9 @@ from .const import ( # noqa: F401 CONF_WS_HEADERS, CONF_WS_PATH, DATA_MQTT, + DEFAULT_DISCOVERY, DEFAULT_ENCODING, + DEFAULT_PREFIX, DEFAULT_QOS, DEFAULT_RETAIN, DOMAIN, @@ -178,7 +180,9 @@ async def _async_setup_discovery( This method is a coroutine. """ - await discovery.async_start(hass, conf[CONF_DISCOVERY_PREFIX], config_entry) + await discovery.async_start( + hass, conf.get(CONF_DISCOVERY_PREFIX, DEFAULT_PREFIX), config_entry + ) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -198,15 +202,8 @@ async def _async_config_entry_updated(hass: HomeAssistant, entry: ConfigEntry) - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Load a config entry.""" - # validate entry config - try: - conf = CONFIG_SCHEMA_ENTRY(dict(entry.data)) - except vol.MultipleInvalid as ex: - raise ConfigEntryError( - f"The MQTT config entry is invalid, please correct it: {ex}" - ) from ex - - # Fetch configuration and add default values + conf = dict(entry.data) + # Fetch configuration hass_config = await conf_util.async_hass_config_yaml(hass) mqtt_yaml = PLATFORM_CONFIG_SCHEMA_BASE(hass_config.get(DOMAIN, {})) client = MQTT(hass, entry, conf) @@ -390,7 +387,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) ) # Setup discovery - if conf.get(CONF_DISCOVERY): + if conf.get(CONF_DISCOVERY, DEFAULT_DISCOVERY): await _async_setup_discovery(hass, conf, entry) # Setup reload service after all platforms have loaded await async_setup_reload_service() diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 27b13a24b7c..2064594fbf0 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -44,7 +44,6 @@ from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util.logging import catch_log_exception from .const import ( - ATTR_TOPIC, CONF_BIRTH_MESSAGE, CONF_BROKER, CONF_CERTIFICATE, @@ -56,10 +55,16 @@ from .const import ( CONF_WILL_MESSAGE, CONF_WS_HEADERS, CONF_WS_PATH, + DEFAULT_BIRTH, DEFAULT_ENCODING, + DEFAULT_KEEPALIVE, + DEFAULT_PORT, DEFAULT_PROTOCOL, DEFAULT_QOS, DEFAULT_TRANSPORT, + DEFAULT_WILL, + DEFAULT_WS_HEADERS, + DEFAULT_WS_PATH, MQTT_CONNECTED, MQTT_DISCONNECTED, PROTOCOL_5, @@ -273,8 +278,8 @@ class MqttClientSetup: client_cert = get_file_path(CONF_CLIENT_CERT, config.get(CONF_CLIENT_CERT)) tls_insecure = config.get(CONF_TLS_INSECURE) if transport == TRANSPORT_WEBSOCKETS: - ws_path: str = config[CONF_WS_PATH] - ws_headers: dict[str, str] = config[CONF_WS_HEADERS] + ws_path: str = config.get(CONF_WS_PATH, DEFAULT_WS_PATH) + ws_headers: dict[str, str] = config.get(CONF_WS_HEADERS, DEFAULT_WS_HEADERS) self._client.ws_set_options(ws_path, ws_headers) if certificate is not None: self._client.tls_set( @@ -452,15 +457,8 @@ class MQTT: self._mqttc.on_subscribe = self._mqtt_on_callback self._mqttc.on_unsubscribe = self._mqtt_on_callback - if ( - CONF_WILL_MESSAGE in self.conf - and ATTR_TOPIC in self.conf[CONF_WILL_MESSAGE] - ): - will_message = PublishMessage(**self.conf[CONF_WILL_MESSAGE]) - else: - will_message = None - - if will_message is not None: + if will := self.conf.get(CONF_WILL_MESSAGE, DEFAULT_WILL): + will_message = PublishMessage(**will) self._mqttc.will_set( topic=will_message.topic, payload=will_message.payload, @@ -503,8 +501,8 @@ class MQTT: result = await self.hass.async_add_executor_job( self._mqttc.connect, self.conf[CONF_BROKER], - self.conf[CONF_PORT], - self.conf[CONF_KEEPALIVE], + self.conf.get(CONF_PORT, DEFAULT_PORT), + self.conf.get(CONF_KEEPALIVE, DEFAULT_KEEPALIVE), ) except OSError as err: _LOGGER.error("Failed to connect to MQTT server due to exception: %s", err) @@ -738,16 +736,13 @@ class MQTT: _LOGGER.info( "Connected to MQTT server %s:%s (%s)", self.conf[CONF_BROKER], - self.conf[CONF_PORT], + self.conf.get(CONF_PORT, DEFAULT_PORT), result_code, ) self.hass.create_task(self._async_resubscribe()) - if ( - CONF_BIRTH_MESSAGE in self.conf - and ATTR_TOPIC in self.conf[CONF_BIRTH_MESSAGE] - ): + if birth := self.conf.get(CONF_BIRTH_MESSAGE, DEFAULT_BIRTH): async def publish_birth_message(birth_message: PublishMessage) -> None: await self._ha_started.wait() # Wait for Home Assistant to start @@ -761,7 +756,7 @@ class MQTT: retain=birth_message.retain, ) - birth_message = PublishMessage(**self.conf[CONF_BIRTH_MESSAGE]) + birth_message = PublishMessage(**birth) asyncio.run_coroutine_threadsafe( publish_birth_message(birth_message), self.hass.loop ) @@ -880,7 +875,7 @@ class MQTT: _LOGGER.warning( "Disconnected from MQTT server %s:%s (%s)", self.conf[CONF_BROKER], - self.conf[CONF_PORT], + self.conf.get(CONF_PORT, DEFAULT_PORT), result_code, ) diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 77c3856aac1..e23280c7f2b 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -47,7 +47,6 @@ from homeassistant.helpers.selector import ( from homeassistant.util.json import JSON_DECODE_EXCEPTIONS, json_loads from .client import MqttClientSetup -from .config_integration import CONFIG_SCHEMA_ENTRY from .const import ( ATTR_PAYLOAD, ATTR_QOS, @@ -369,7 +368,6 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): updated_config = {} updated_config.update(self.broker_config) updated_config.update(options_config) - CONFIG_SCHEMA_ENTRY(updated_config) self.hass.config_entries.async_update_entry( self.config_entry, data=updated_config, diff --git a/homeassistant/components/mqtt/config_integration.py b/homeassistant/components/mqtt/config_integration.py index 443fc070fa1..469f52e1488 100644 --- a/homeassistant/components/mqtt/config_integration.py +++ b/homeassistant/components/mqtt/config_integration.py @@ -45,23 +45,8 @@ from .const import ( CONF_DISCOVERY_PREFIX, CONF_KEEPALIVE, CONF_TLS_INSECURE, - CONF_TRANSPORT, CONF_WILL_MESSAGE, - CONF_WS_HEADERS, - CONF_WS_PATH, - DEFAULT_BIRTH, - DEFAULT_DISCOVERY, - DEFAULT_KEEPALIVE, - DEFAULT_PORT, - DEFAULT_PREFIX, - DEFAULT_PROTOCOL, - DEFAULT_TRANSPORT, - DEFAULT_WILL, - SUPPORTED_PROTOCOLS, - TRANSPORT_TCP, - TRANSPORT_WEBSOCKETS, ) -from .util import valid_birth_will, valid_publish_topic DEFAULT_TLS_PROTOCOL = "auto" @@ -155,41 +140,6 @@ CLIENT_KEY_AUTH_MSG = ( "client_key and client_cert must both be present in the MQTT broker configuration" ) -CONFIG_SCHEMA_ENTRY = vol.Schema( - { - vol.Optional(CONF_CLIENT_ID): cv.string, - vol.Optional(CONF_KEEPALIVE, default=DEFAULT_KEEPALIVE): vol.All( - vol.Coerce(int), vol.Range(min=15) - ), - vol.Required(CONF_BROKER): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_USERNAME): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, - vol.Optional(CONF_CERTIFICATE): str, - vol.Inclusive(CONF_CLIENT_KEY, "client_key_auth", msg=CLIENT_KEY_AUTH_MSG): str, - vol.Inclusive( - CONF_CLIENT_CERT, "client_key_auth", msg=CLIENT_KEY_AUTH_MSG - ): str, - vol.Optional(CONF_TLS_INSECURE): cv.boolean, - vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): vol.All( - cv.string, vol.In(SUPPORTED_PROTOCOLS) - ), - vol.Optional(CONF_WILL_MESSAGE, default=DEFAULT_WILL): valid_birth_will, - vol.Optional(CONF_BIRTH_MESSAGE, default=DEFAULT_BIRTH): valid_birth_will, - vol.Optional(CONF_DISCOVERY, default=DEFAULT_DISCOVERY): cv.boolean, - # discovery_prefix must be a valid publish topic because if no - # state topic is specified, it will be created with the given prefix. - vol.Optional( - CONF_DISCOVERY_PREFIX, default=DEFAULT_PREFIX - ): valid_publish_topic, - vol.Optional(CONF_TRANSPORT, default=DEFAULT_TRANSPORT): vol.All( - cv.string, vol.In([TRANSPORT_TCP, TRANSPORT_WEBSOCKETS]) - ), - vol.Optional(CONF_WS_PATH, default="/"): cv.string, - vol.Optional(CONF_WS_HEADERS, default={}): {cv.string: cv.string}, - } -) - DEPRECATED_CONFIG_KEYS = [ CONF_BIRTH_MESSAGE, CONF_BROKER, diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 41fd353359e..fe7c6eb4cf0 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -46,6 +46,7 @@ DEFAULT_PAYLOAD_AVAILABLE = "online" DEFAULT_PAYLOAD_NOT_AVAILABLE = "offline" DEFAULT_PORT = 1883 DEFAULT_RETAIN = False +DEFAULT_WS_HEADERS: dict[str, str] = {} DEFAULT_WS_PATH = "/" PROTOCOL_31 = "3.1" diff --git a/tests/components/mqtt/test_diagnostics.py b/tests/components/mqtt/test_diagnostics.py index 386fb15f1f4..3eec6887b1a 100644 --- a/tests/components/mqtt/test_diagnostics.py +++ b/tests/components/mqtt/test_diagnostics.py @@ -19,20 +19,6 @@ from tests.typing import ClientSessionGenerator, MqttMockHAClientGenerator default_config = { "birth_message": {}, "broker": "mock-broker", - "discovery": True, - "discovery_prefix": "homeassistant", - "keepalive": 60, - "port": 1883, - "protocol": "3.1.1", - "transport": "tcp", - "will_message": { - "payload": "offline", - "qos": 0, - "retain": False, - "topic": "homeassistant/status", - }, - "ws_headers": {}, - "ws_path": "/", } @@ -57,6 +43,7 @@ async def test_entry_diagnostics( config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] mqtt_mock.connected = True + await get_diagnostics_for_config_entry(hass, hass_client, config_entry) assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { "connected": True, "devices": [], diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 5337668e016..3cab3d63f68 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -2288,23 +2288,6 @@ async def test_default_entry_setting_are_applied( assert device_entry is not None -async def test_fail_no_broker( - hass: HomeAssistant, - device_registry: dr.DeviceRegistry, - mqtt_client_mock: MqttMockPahoClient, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test the MQTT entry setup when broker configuration is missing.""" - # Config entry data is incomplete - entry = MockConfigEntry(domain=mqtt.DOMAIN, data={}) - entry.add_to_hass(hass) - assert not await hass.config_entries.async_setup(entry.entry_id) - assert ( - "The MQTT config entry is invalid, please correct it: required key not provided @ data['broker']" - in caplog.text - ) - - @pytest.mark.no_fail_on_log_exception async def test_message_callback_exception_gets_logged( hass: HomeAssistant, @@ -3312,41 +3295,16 @@ async def test_setup_manual_items_with_unique_ids( assert bool("Platform mqtt does not generate unique IDs." in caplog.text) != unique -async def test_fail_with_unknown_conf_entry_options( - hass: HomeAssistant, - mqtt_client_mock: MqttMockPahoClient, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test unknown keys in config entry data is removed.""" - mqtt_config_entry_data = { - mqtt.CONF_BROKER: "mock-broker", - mqtt.CONF_BIRTH_MESSAGE: {}, - "old_option": "old_value", - } - - entry = MockConfigEntry( - data=mqtt_config_entry_data, - domain=mqtt.DOMAIN, - title="MQTT", - ) - - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) is False - - assert ("extra keys not allowed @ data['old_option']") in caplog.text - - -@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.LIGHT]) @pytest.mark.parametrize( "hass_config", [ { "mqtt": { - "light": [ + "sensor": [ { "name": "test_manual", "unique_id": "test_manual_unique_id123", - "command_topic": "test-topic_manual", + "state_topic": "test-topic_manual", } ] } @@ -3366,15 +3324,16 @@ async def test_link_config_entry( config_discovery = { "name": "test_discovery", "unique_id": "test_discovery_unique456", - "command_topic": "test-topic_discovery", + "state_topic": "test-topic_discovery", } async_fire_mqtt_message( - hass, "homeassistant/light/bla/config", json.dumps(config_discovery) + hass, "homeassistant/sensor/bla/config", json.dumps(config_discovery) ) await hass.async_block_till_done() + await hass.async_block_till_done() - assert hass.states.get("light.test_manual") is not None - assert hass.states.get("light.test_discovery") is not None + assert hass.states.get("sensor.test_manual") is not None + assert hass.states.get("sensor.test_discovery") is not None entity_names = ["test_manual", "test_discovery"] # Check if both entities were linked to the MQTT config entry @@ -3402,7 +3361,7 @@ async def test_link_config_entry( assert _check_entities() == 1 # set up item through discovery async_fire_mqtt_message( - hass, "homeassistant/light/bla/config", json.dumps(config_discovery) + hass, "homeassistant/sensor/bla/config", json.dumps(config_discovery) ) await hass.async_block_till_done() assert _check_entities() == 2 From eb63bc79678a0d0b61ee72078b8f8e2b0470674c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 11 Apr 2023 19:54:30 +0200 Subject: [PATCH 09/10] Fix switch_as_x name (#91232) --- homeassistant/components/switch_as_x/cover.py | 7 +- .../components/switch_as_x/entity.py | 21 ++- homeassistant/components/switch_as_x/fan.py | 3 +- homeassistant/components/switch_as_x/light.py | 7 +- homeassistant/components/switch_as_x/lock.py | 3 +- homeassistant/components/switch_as_x/siren.py | 7 +- tests/components/switch_as_x/test_init.py | 136 ++++++++++++++++++ 7 files changed, 176 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/switch_as_x/cover.py b/homeassistant/components/switch_as_x/cover.py index b7f8e5bf971..7df3b177217 100644 --- a/homeassistant/components/switch_as_x/cover.py +++ b/homeassistant/components/switch_as_x/cover.py @@ -3,7 +3,11 @@ from __future__ import annotations from typing import Any -from homeassistant.components.cover import CoverEntity, CoverEntityFeature +from homeassistant.components.cover import ( + DOMAIN as COVER_DOMAIN, + CoverEntity, + CoverEntityFeature, +) from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -36,6 +40,7 @@ async def async_setup_entry( CoverSwitch( hass, config_entry.title, + COVER_DOMAIN, entity_id, config_entry.entry_id, ) diff --git a/homeassistant/components/switch_as_x/entity.py b/homeassistant/components/switch_as_x/entity.py index 8432c46f856..21a7b882442 100644 --- a/homeassistant/components/switch_as_x/entity.py +++ b/homeassistant/components/switch_as_x/entity.py @@ -23,13 +23,15 @@ class BaseEntity(Entity): """Represents a Switch as an X.""" _attr_should_poll = False + _is_new_entity: bool def __init__( self, hass: HomeAssistant, config_entry_title: str, + domain: str, switch_entity_id: str, - unique_id: str | None, + unique_id: str, ) -> None: """Initialize Switch as an X.""" registry = er.async_get(hass) @@ -41,7 +43,7 @@ class BaseEntity(Entity): name: str | None = config_entry_title if wrapped_switch: - name = wrapped_switch.name or wrapped_switch.original_name + name = wrapped_switch.original_name self._device_id = device_id if device_id and (device := device_registry.async_get(device_id)): @@ -55,6 +57,10 @@ class BaseEntity(Entity): self._attr_unique_id = unique_id self._switch_entity_id = switch_entity_id + self._is_new_entity = ( + registry.async_get_entity_id(domain, SWITCH_AS_X_DOMAIN, unique_id) is None + ) + @callback def async_state_changed_listener(self, event: Event | None = None) -> None: """Handle child updates.""" @@ -67,7 +73,7 @@ class BaseEntity(Entity): self._attr_available = True async def async_added_to_hass(self) -> None: - """Register callbacks.""" + """Register callbacks and copy the wrapped entity's custom name if set.""" @callback def _async_state_changed_listener(event: Event | None = None) -> None: @@ -93,6 +99,15 @@ class BaseEntity(Entity): {"entity_id": self._switch_entity_id}, ) + if not self._is_new_entity: + return + + wrapped_switch = registry.async_get(self._switch_entity_id) + if not wrapped_switch or wrapped_switch.name is None: + return + + registry.async_update_entity(self.entity_id, name=wrapped_switch.name) + class BaseToggleEntity(BaseEntity, ToggleEntity): """Represents a Switch as a ToggleEntity.""" diff --git a/homeassistant/components/switch_as_x/fan.py b/homeassistant/components/switch_as_x/fan.py index 87a6c387295..d8c43cfe381 100644 --- a/homeassistant/components/switch_as_x/fan.py +++ b/homeassistant/components/switch_as_x/fan.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Any -from homeassistant.components.fan import FanEntity +from homeassistant.components.fan import DOMAIN as FAN_DOMAIN, FanEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ENTITY_ID from homeassistant.core import HomeAssistant @@ -29,6 +29,7 @@ async def async_setup_entry( FanSwitch( hass, config_entry.title, + FAN_DOMAIN, entity_id, config_entry.entry_id, ) diff --git a/homeassistant/components/switch_as_x/light.py b/homeassistant/components/switch_as_x/light.py index 7bcdb659e9c..e6183c95d91 100644 --- a/homeassistant/components/switch_as_x/light.py +++ b/homeassistant/components/switch_as_x/light.py @@ -1,7 +1,11 @@ """Light support for switch entities.""" from __future__ import annotations -from homeassistant.components.light import ColorMode, LightEntity +from homeassistant.components.light import ( + DOMAIN as LIGHT_DOMAIN, + ColorMode, + LightEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ENTITY_ID from homeassistant.core import HomeAssistant @@ -27,6 +31,7 @@ async def async_setup_entry( LightSwitch( hass, config_entry.title, + LIGHT_DOMAIN, entity_id, config_entry.entry_id, ) diff --git a/homeassistant/components/switch_as_x/lock.py b/homeassistant/components/switch_as_x/lock.py index e3c29a1cf42..9778caf8e60 100644 --- a/homeassistant/components/switch_as_x/lock.py +++ b/homeassistant/components/switch_as_x/lock.py @@ -3,7 +3,7 @@ from __future__ import annotations from typing import Any -from homeassistant.components.lock import LockEntity +from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN, LockEntity from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -36,6 +36,7 @@ async def async_setup_entry( LockSwitch( hass, config_entry.title, + LOCK_DOMAIN, entity_id, config_entry.entry_id, ) diff --git a/homeassistant/components/switch_as_x/siren.py b/homeassistant/components/switch_as_x/siren.py index 88ff9a322d3..c9981b17cfe 100644 --- a/homeassistant/components/switch_as_x/siren.py +++ b/homeassistant/components/switch_as_x/siren.py @@ -1,7 +1,11 @@ """Siren support for switch entities.""" from __future__ import annotations -from homeassistant.components.siren import SirenEntity, SirenEntityFeature +from homeassistant.components.siren import ( + DOMAIN as SIREN_DOMAIN, + SirenEntity, + SirenEntityFeature, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ENTITY_ID from homeassistant.core import HomeAssistant @@ -27,6 +31,7 @@ async def async_setup_entry( SirenSwitch( hass, config_entry.title, + SIREN_DOMAIN, entity_id, config_entry.entry_id, ) diff --git a/tests/components/switch_as_x/test_init.py b/tests/components/switch_as_x/test_init.py index 2d63ce9617b..87cc291a599 100644 --- a/tests/components/switch_as_x/test_init.py +++ b/tests/components/switch_as_x/test_init.py @@ -534,7 +534,143 @@ async def test_entity_name( assert entity_entry assert entity_entry.device_id == switch_entity_entry.device_id assert entity_entry.has_entity_name is True + assert entity_entry.name is None assert entity_entry.original_name is None assert entity_entry.options == { DOMAIN: {"entity_id": switch_entity_entry.entity_id} } + + +@pytest.mark.parametrize("target_domain", PLATFORMS_TO_TEST) +async def test_custom_name_1( + hass: HomeAssistant, + target_domain: Platform, +) -> None: + """Test the source entity has a custom name.""" + registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + switch_config_entry = MockConfigEntry() + + device_entry = device_registry.async_get_or_create( + config_entry_id=switch_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + name="Device name", + ) + + switch_entity_entry = registry.async_get_or_create( + "switch", + "test", + "unique", + device_id=device_entry.id, + has_entity_name=True, + original_name="Original entity name", + ) + switch_entity_entry = registry.async_update_entity( + switch_entity_entry.entity_id, + config_entry_id=switch_config_entry.entry_id, + name="Custom entity name", + ) + + # Add the config entry + switch_as_x_config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + CONF_ENTITY_ID: switch_entity_entry.id, + CONF_TARGET_DOMAIN: target_domain, + }, + title="ABC", + ) + switch_as_x_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(switch_as_x_config_entry.entry_id) + await hass.async_block_till_done() + + entity_entry = registry.async_get( + f"{target_domain}.device_name_original_entity_name" + ) + assert entity_entry + assert entity_entry.device_id == switch_entity_entry.device_id + assert entity_entry.has_entity_name is True + assert entity_entry.name == "Custom entity name" + assert entity_entry.original_name == "Original entity name" + assert entity_entry.options == { + DOMAIN: {"entity_id": switch_entity_entry.entity_id} + } + + +@pytest.mark.parametrize("target_domain", PLATFORMS_TO_TEST) +async def test_custom_name_2( + hass: HomeAssistant, + target_domain: Platform, +) -> None: + """Test the source entity has a custom name. + + This tests the custom name is only copied from the source device when the config + switch_as_x config entry is setup the first time. + """ + registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + switch_config_entry = MockConfigEntry() + + device_entry = device_registry.async_get_or_create( + config_entry_id=switch_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + name="Device name", + ) + + switch_entity_entry = registry.async_get_or_create( + "switch", + "test", + "unique", + device_id=device_entry.id, + has_entity_name=True, + original_name="Original entity name", + ) + switch_entity_entry = registry.async_update_entity( + switch_entity_entry.entity_id, + config_entry_id=switch_config_entry.entry_id, + name="New custom entity name", + ) + + # Add the config entry + switch_as_x_config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + CONF_ENTITY_ID: switch_entity_entry.id, + CONF_TARGET_DOMAIN: target_domain, + }, + title="ABC", + ) + switch_as_x_config_entry.add_to_hass(hass) + + switch_as_x_entity_entry = registry.async_get_or_create( + target_domain, + "switch_as_x", + switch_as_x_config_entry.entry_id, + suggested_object_id="device_name_original_entity_name", + ) + switch_as_x_entity_entry = registry.async_update_entity( + switch_as_x_entity_entry.entity_id, + config_entry_id=switch_config_entry.entry_id, + name="Old custom entity name", + ) + + assert await hass.config_entries.async_setup(switch_as_x_config_entry.entry_id) + await hass.async_block_till_done() + + entity_entry = registry.async_get( + f"{target_domain}.device_name_original_entity_name" + ) + assert entity_entry + assert entity_entry.entity_id == switch_as_x_entity_entry.entity_id + assert entity_entry.device_id == switch_entity_entry.device_id + assert entity_entry.has_entity_name is True + assert entity_entry.name == "Old custom entity name" + assert entity_entry.original_name == "Original entity name" + assert entity_entry.options == { + DOMAIN: {"entity_id": switch_entity_entry.entity_id} + } From 3a72054f930a6c1d4bc1c960a292b79dc7c0e6f2 Mon Sep 17 00:00:00 2001 From: rlippmann <70883373+rlippmann@users.noreply.github.com> Date: Tue, 11 Apr 2023 13:58:28 -0400 Subject: [PATCH 10/10] Make dataclasses in HA core slotted (#91208) --- homeassistant/exceptions.py | 8 ++++---- homeassistant/helpers/collection.py | 2 +- homeassistant/helpers/entity.py | 4 ++-- homeassistant/helpers/event.py | 6 +++--- homeassistant/helpers/integration_platform.py | 2 +- homeassistant/helpers/intent.py | 2 +- homeassistant/helpers/issue_registry.py | 2 +- homeassistant/helpers/recorder.py | 2 +- homeassistant/helpers/schema_config_entry_flow.py | 4 ++-- homeassistant/helpers/service.py | 2 +- homeassistant/helpers/service_info/mqtt.py | 2 +- homeassistant/helpers/trigger.py | 2 +- homeassistant/loader.py | 2 +- homeassistant/runner.py | 2 +- homeassistant/util/yaml/objects.py | 2 +- 15 files changed, 22 insertions(+), 22 deletions(-) diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py index 6cc93ef4f66..bfc96eabfdf 100644 --- a/homeassistant/exceptions.py +++ b/homeassistant/exceptions.py @@ -32,7 +32,7 @@ class TemplateError(HomeAssistantError): super().__init__(f"{exception.__class__.__name__}: {exception}") -@dataclass +@dataclass(slots=True) class ConditionError(HomeAssistantError): """Error during condition evaluation.""" @@ -52,7 +52,7 @@ class ConditionError(HomeAssistantError): return "\n".join(list(self.output(indent=0))) -@dataclass +@dataclass(slots=True) class ConditionErrorMessage(ConditionError): """Condition error message.""" @@ -64,7 +64,7 @@ class ConditionErrorMessage(ConditionError): yield self._indent(indent, f"In '{self.type}' condition: {self.message}") -@dataclass +@dataclass(slots=True) class ConditionErrorIndex(ConditionError): """Condition error with index.""" @@ -87,7 +87,7 @@ class ConditionErrorIndex(ConditionError): yield from self.error.output(indent + 1) -@dataclass +@dataclass(slots=True) class ConditionErrorContainer(ConditionError): """Condition error with subconditions.""" diff --git a/homeassistant/helpers/collection.py b/homeassistant/helpers/collection.py index 4d5dc4012ee..29151221a89 100644 --- a/homeassistant/helpers/collection.py +++ b/homeassistant/helpers/collection.py @@ -35,7 +35,7 @@ CHANGE_REMOVED = "removed" _T = TypeVar("_T") -@dataclass +@dataclass(slots=True) class CollectionChangeSet: """Class to represent a change set. diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 8352c1e4463..e5ef3bd4574 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -205,7 +205,7 @@ class EntityPlatformState(Enum): REMOVED = auto() -@dataclass +@dataclass(slots=True) class EntityDescription: """A class that describes Home Assistant entities.""" @@ -981,7 +981,7 @@ class Entity(ABC): return report_issue -@dataclass +@dataclass(slots=True) class ToggleEntityDescription(EntityDescription): """A class that describes toggle entities.""" diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 44a9cb087e3..27258d262ba 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -66,7 +66,7 @@ RANDOM_MICROSECOND_MAX = 500000 _P = ParamSpec("_P") -@dataclass +@dataclass(slots=True) class TrackStates: """Class for keeping track of states being tracked. @@ -80,7 +80,7 @@ class TrackStates: domains: set[str] -@dataclass +@dataclass(slots=True) class TrackTemplate: """Class for keeping track of a template with variables. @@ -94,7 +94,7 @@ class TrackTemplate: rate_limit: timedelta | None = None -@dataclass +@dataclass(slots=True) class TrackTemplateResult: """Class for result of template tracking. diff --git a/homeassistant/helpers/integration_platform.py b/homeassistant/helpers/integration_platform.py index ef05dae518b..ddaede44962 100644 --- a/homeassistant/helpers/integration_platform.py +++ b/homeassistant/helpers/integration_platform.py @@ -16,7 +16,7 @@ _LOGGER = logging.getLogger(__name__) DATA_INTEGRATION_PLATFORMS = "integration_platforms" -@dataclass(frozen=True) +@dataclass(slots=True, frozen=True) class IntegrationPlatform: """An integration platform.""" diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index 4e7dcc5a5a1..7a4ca862ee2 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -568,7 +568,7 @@ class IntentResponseTargetType(str, Enum): CUSTOM = "custom" -@dataclass +@dataclass(slots=True) class IntentResponseTarget: """Target of the intent response.""" diff --git a/homeassistant/helpers/issue_registry.py b/homeassistant/helpers/issue_registry.py index 345ec099d3f..afe2d98ed0b 100644 --- a/homeassistant/helpers/issue_registry.py +++ b/homeassistant/helpers/issue_registry.py @@ -32,7 +32,7 @@ class IssueSeverity(StrEnum): WARNING = "warning" -@dataclasses.dataclass(frozen=True) +@dataclasses.dataclass(slots=True, frozen=True) class IssueEntry: """Issue Registry Entry.""" diff --git a/homeassistant/helpers/recorder.py b/homeassistant/helpers/recorder.py index 5545aa09f01..74ebbe5c67a 100644 --- a/homeassistant/helpers/recorder.py +++ b/homeassistant/helpers/recorder.py @@ -9,7 +9,7 @@ from homeassistant.core import HomeAssistant, callback DOMAIN = "recorder" -@dataclass +@dataclass(slots=True) class RecorderData: """Recorder data stored in hass.data.""" diff --git a/homeassistant/helpers/schema_config_entry_flow.py b/homeassistant/helpers/schema_config_entry_flow.py index 5101e5c69a7..653594f2808 100644 --- a/homeassistant/helpers/schema_config_entry_flow.py +++ b/homeassistant/helpers/schema_config_entry_flow.py @@ -27,7 +27,7 @@ class SchemaFlowStep: """Define a config or options flow step.""" -@dataclass +@dataclass(slots=True) class SchemaFlowFormStep(SchemaFlowStep): """Define a config or options flow form step.""" @@ -79,7 +79,7 @@ class SchemaFlowFormStep(SchemaFlowStep): """ -@dataclass +@dataclass(slots=True) class SchemaFlowMenuStep(SchemaFlowStep): """Define a config or options flow menu step.""" diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 33c677454bc..14cf6a85a24 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -199,7 +199,7 @@ class ServiceTargetSelector: return bool(self.entity_ids or self.device_ids or self.area_ids) -@dataclasses.dataclass +@dataclasses.dataclass(slots=True) class SelectedEntities: """Class to hold the selected entities.""" diff --git a/homeassistant/helpers/service_info/mqtt.py b/homeassistant/helpers/service_info/mqtt.py index 3626f9b5758..906072a2d4b 100644 --- a/homeassistant/helpers/service_info/mqtt.py +++ b/homeassistant/helpers/service_info/mqtt.py @@ -7,7 +7,7 @@ from homeassistant.data_entry_flow import BaseServiceInfo ReceivePayloadType = str | bytes -@dataclass +@dataclass(slots=True) class MqttServiceInfo(BaseServiceInfo): """Prepared info from mqtt entries.""" diff --git a/homeassistant/helpers/trigger.py b/homeassistant/helpers/trigger.py index e2963b15ab4..40e1860b409 100644 --- a/homeassistant/helpers/trigger.py +++ b/homeassistant/helpers/trigger.py @@ -95,7 +95,7 @@ class TriggerInfo(TypedDict): trigger_data: TriggerData -@dataclass +@dataclass(slots=True) class PluggableActionsEntry: """Holder to keep track of all plugs and actions for a given trigger.""" diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 11f551b37eb..963ddcf48fc 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -123,7 +123,7 @@ class USBMatcher(USBMatcherRequired, USBMatcherOptional): """Matcher for the bluetooth integration.""" -@dataclass +@dataclass(slots=True) class HomeKitDiscoveredIntegration: """HomeKit model.""" diff --git a/homeassistant/runner.py b/homeassistant/runner.py index e5a87a4b092..9a86bed7594 100644 --- a/homeassistant/runner.py +++ b/homeassistant/runner.py @@ -34,7 +34,7 @@ ALPINE_RELEASE_FILE = "/etc/alpine-release" _LOGGER = logging.getLogger(__name__) -@dataclasses.dataclass +@dataclasses.dataclass(slots=True) class RuntimeConfig: """Class to hold the information for running Home Assistant.""" diff --git a/homeassistant/util/yaml/objects.py b/homeassistant/util/yaml/objects.py index e7b262ad496..b2320a74d2c 100644 --- a/homeassistant/util/yaml/objects.py +++ b/homeassistant/util/yaml/objects.py @@ -18,7 +18,7 @@ class NodeDictClass(dict): """Wrapper class to be able to add attributes on a dict.""" -@dataclass(frozen=True) +@dataclass(slots=True, frozen=True) class Input: """Input that should be substituted."""